Artikel top billede

(Foto: Computerworld)

Guide: Opret din egen digitale assistent

I denne guide ser vi på det, der skal til for at lave din egen stemmestyrings-software til dine Raspberry Pi-projekter. Hvis du ønsker dig en virtuel assistent, kan du overveje Jasper-systemet.

Af Torben Okholm, Alt om Data

Denne artikel er oprindeligt bragt på Alt om Data. Computerworld overtog i november 2022 Alt om Data. Du kan læse mere om overtagelsen her.

Dokumentationen på Jaspers website rummer en beskrivelse af den hardware, man skal knytte til sin Raspberry Pi, og her er der også et komplet sæt anvisninger til installation og konfiguration. Der findes en samling af standardmoduler, som muliggør interaktion med forskellige tjenester – du kan bruge modulerne time, Gmail eller sågar joke – og der er også tredjepartsmoduler, som man kan bruge. Der er tilmed et udvikler-API og dokumentation, der hjælper dig med at føje funktionalitet til Jasper.

1 Stemmestyring

Enhver, der har set Iron Man-filmene, har sikkert drømt om at have sit eget kunstig intelligens-system til at parere ordre. Filmenes J.A.R.V.I.S.-computer har enorme mængder af computerkraft, men man kan selv konstruere noget lignende med meget beskedne ressourcer.

Ved hjælp af en Raspberry Pi og programmeringssproget Python kan du lave din egen personlige assistent, som du kan bruge som facade for de enorme supercomputer-ressourcer, du bruger i din hverdag som genial playboy og filantrop. På de følgende sider gennemgår vi det grundlæggende, du har brug for at vide – når du er nået til vejs ende, bør du være i stand til at bygge din egen rudimentære, skræddersyede agent.

>> Det første element i systemets interaktion med mennesker består i at lytte efter verbale kommandoer, således at vi ved, hvad der skal bearbejdes. Der er flere mulige tilgange til denne opgave. For ikke at komplicere sagerne håndterer vi her kun enheder, der er sluttet til en af din Raspberry Pis USB-porte. Med den begrænsning kan man tale direkte med USB-enheden på de laveste niveauer. Det kan være nødvendigt, hvis man prøver at bruge noget usædvanligt til at stå for lytningen, men man er formentlig bedre faren, hvis man bruger noget mere almindeligt. I dette tilfælde kan du bruge Python-modulet PyAudio. PyAudio leverer en Python-wrapper til biblioteker PortAudio, der fungerer på tværs af platforme. Hvis vi antager, at du har noget i retning af Raspbian til din Linux-distribution, kan du nemt installere den nødvendige software med denne kommando:
sudo apt-get install python-pyaudio

>> Hvis du skal bruge den seneste version, kan du altid hente den fra kilden. PyAudio rummer funktionalitet til at indlæse audiodata fra en mikrofon plus evnen til at afspille audiodata til hovedtelefoner eller højttalere. Derfor bruger vi det som vores vigtigste form for interaktion med computeren.

>> Det første skridt består i at kunne indlæse nogle audiokommandoer fra de mennesker, der tilfældigvis er i nærheden. Du skal importere PyAudio-modulet, før du kan begynde at interagere med mikrofonen. Arbejdet med PyAudio minder om arbejde med filer, og det burde derfor virke bekendt for de fleste programmører. Du kan begynde med at oprette et nyt PyAudio-objekt med statementet p = pyaudio.PyAudio() . Dernæst kan du åbne en input-stream med funktionen p.open(…) med adskillige parametre. Du kan vælge dataformatet for optagelsen. I vores eksempelkode har vi brugt format=pyaudio.paInt16 . Man kan indstille frekvensen i hertz. For eksempel bruger vi rate=44100 , der er en standard-samplingrate på 44,1 KHz.

Du skal også sige, hvor stor en buffer du vil bruge til optagelsen – vi brugte frames_per_buffer=1024 . Eftersom vi ønsker at optage, skal du bruge input=true . Den sidste parameter står for at vælge antallet af kanaler, der skal optages på. I dette tilfælde bruger vi channels=2 . Nu, da din stream er blevet åbnet, kan du begynde at læse fra den. Du skal indlæse dine audiodata med den samme chunk-størrelse, som du brugte, da du oprettede streamen – den ser ud som stream.read(1024) . Så kan du simpelthen loope og læse, indtil du er færdig. Og så findes der to kommandoer til at lukke input-streamen. Du skal kalde stream.stop_stream() og derefter stream.close() . Hvis du er helt færdig, kan du nu kalde p.terminate() for at lukke for forbindelsen til audioenhederne på din Raspberry Pi.

>> Det næste skridt består i at kunne sende audiooutput, således at J.A.R.V.I.S. også kan tale til dig. Hertil skal du bruge PyAudio, og vi behøver derfor ikke se på et andet Python-modul. For enkelhedens skyld siger vi, at du har en WAV-fil, som du vil afspille. Du kan bruge Python-modulet “wave” til at indlæse den. Igen skal du oprette et PyAudio-objekt og åbne en stream. Parameteren “output” skal være sat til true. Formatet, antallet af kanaler og frekvensen er altsammen information, som er afledt fra de audiodata, der er lagret i din WAV-fil. Når du vil høre din audio, kan du simpelthen loope igennem og læse en mængde data fra WAV-filen ad gangen og skrive den ud til PyAudio-streamen. Når du er færdig, kan du stoppe streamen og lukke den, som du gjorde tidligere.

>> I begge de to nævnte tilfælde blokerer funktionerne, når du kalder dem, indtil de er færdige. Hvilke muligheder har du så, hvis du vil arbejde, mens du enten optager eller outputter audio? Der findes ikkeblokerings-versioner, der tager en callback-funktion som en ekstra parameter, som hedder stream_callback . Denne callback-funktion tager fire parametre, der hedder in_data , frame_count , time_info og status . Parameteren in_data indeholder den optagede audio, hvis input er true. Callback-funktionen skal returnere en tupel med værdierne out_data og flag — out_data indeholder de data, der skal outputtes, hvis output er true i kaldet til funktionen open.

Hvis inputtet i stedet er true, skal out_data være lig med None. Flager kan være paContinue , paComplete eller paAbort  – med indlysende betydninger. Man skal være opmærksom på, at man ikke kan kalde, læse eller skrive funktioner, når man ønsker at at bruge en callback-funktion. Når streamen er åbnet, kan man simpelthen kalde funktionen stream.start_stream() . Det indleder en separat thread, der håndterer denne stream-bearbejdelse. Man kan bruge stream.is_active() til at tjekke den aktuelle status. Når stream-bearbejdelsen er færdig, kan du kalde stream.stop_stream() for at stoppe den sekundære thread.

stemmestyring: komplet kodeoversigt

# Du skal importere PyAudio-modulet
import pyaudio

# Først skal vi lytte
# Vi skal angive nogle parametre
# Buffer-chunkstørrelse i
CHUNK = 1024
# Audioformatet
FORMAT = pyaudio.paInt16
# Antallet af kanaler, der skal optages på
CHANNELS = 2
# Samplerate, 44,1 KHz
RATE = 44100
# Antallet af sekunder, der skal optages i
RECORD_SECS = 5

# Nu skal vi oprette et PyAudio-objekt
p = pyaudio.PyAudio()

# Vi skal have en stream, vi kan optage fra
stream = p.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=TRUE, frames_per_buffer=CHUNK)

# Nu kan vi optage ind i en midlertidig buffer
frames = []
for i in range(0, int(RATE / CHUNK * RECORD_SECS)):
data = stream.read(CHUNK)
frames.append(data)

# Nu kan vi lukke for det hele
stream.stop_stream()
stream.close()
p.terminate()

# Hvis vi vil spille en wave-fil, skal vi bruge wave-modulet
import wave

# Vi kan åbne det og give det et filnavn
wf = wave.open(“filename.wav”, “rb”)

# Vi skal have et nyt PyAudio-objekt
p = pyaudio.PyAudio()

# Vi åbner en stream med indstillingerne fra wave-filen
stream = p.open(format=p.get_format_from_width(wf.
getsampwidth()),channels=wf.getnchannels(), rate=wf.
getframerate(),output=True)

# Nu kan vi læse fra filen og afspille den
data = wf.readframes(CHUNK)
while data != ‘’:
stream.write(data)
data = wf.readframes(CHUNK)

# Glem ikke at lukke for det hele til sidst
stream.stop_stream()
stream.close()
p.terminate()

2 Udliciter opgaverne

Du kan overlade bearbejdelsen af audiodata til Google og gå direkte til API-’et over HTTP ved at sende dine audiodata til den relevante URL. Først skal du installere Python-modulet SpeechRecognition:
pip install SpeechRecognition

>> Opret nu en forekomst af Recognizer-objektet. Et Helper-objekt ved navn WavFile tager en audiofil og forbereder den på brug med Google API’et. Så bearbejder det den med funktionen record() og sender den bearbejdede audio til funktionen recognize() . Når den kommer tilbage, får du en liste over mulige tekstpar plus et procentdels-tillidsniveau for hver mulig tekst-dekoder. Vær opmærksom på, at dette modul bruger en uofficiel API-nøgle til at stå for afkodningen. Når det gælder alt andet end beskeden personlig test, skal du derfor hente din egen API-nøgle.

>> Vi har set, hvordan vi kan få vores Raspberry Pi til at lytte til verden omkring den, og nu skal vi finde ud af, hvad det er, den lige har hørt. Det er det, man nomalt kalder stemmegenkendelse, og det er et meget stort og aktivt forskningsområde. Alle de vigtige smartphone-operatisystemer har applikationer, der prøver at udnytte denne form for menneskelig interaktion. Der findes også adskillige forskellige Python-moduler, der kan udføre dette oversættelsestrin (speech-to-text, STT). I denne del af vores projekt ser vi på brugen af Pocket Sphinx til at klare alt det tunge arbejde. Sphinx blev udviklet af Carnegie Mellon University, og det er licenseret under en BSD-licens. Det betyder, at man frit kan tilføje ekstra funktionalitet, som man måtte have brug for til specifikke opgaver. På grund af aktiviteten på dette felt er det en god ide at holde rede på alle opdateringerne og forbedringer i ydelsen.

>> Man kan downloade kildekoden til alle disse moduler og bygge det hele fra grunden, men vi antager, at du bruger en af de Debian-baserede distributioner såsom Raspbian. Her kan man simpelthen bruge det følgende for at skaffe alle de fornødne filer:
sudo apt-get install python-pocketsphinx

>> Du skal også bruge audiomodel-filer og sprogmodel-filer for at få en oversættelse til dit foretrukne sprog. Hvis du vil have filerne til engelsk, kan du installere denne pakke:
sudo apt-get install pocketsphinx-hmm-wsj1
pocketsphinx-lm-wsj

>> Du kan være nødt til at gå uden for det regulære pakke-managementsystem, hvis du vil bruge andre sprog. Så kan du simpelthen begynde at skrive og bruge din kode uden videre. Når du begynder at bruge disse moduler, skal du importere både Pocket Sphinx og Sphinx Base med de følgende kommandoer:

import pocketphinx as ps
import sphinxbase

Disse moduler er i virkeligheden Python-wrappers om den C-kode, der håndterer det egentlige computerarbejde med at oversætte lyde til tekst. Det mest grundlæggende arbejde består i at oprette et Decoder-objekt fra Pocket Sphinx-modulet. Decoder-objektet bruger adskillige input-parametre til at definere de sprogfiler, som det har lov til at bruge. De omfatter hmm , lm og dict . Hvis du bruger de ovennævnte pakker til håndtering af engelsk, kan du finde de nødvendige filer i mapperne “/usr/share/pocketsphinx/model/hmm/wsj1” og
“/usr/share/pocketsphinx/model/lm/wsj.”

Hvis du ikke angiver disse parametre, prøver det at bruge fornuftige standardfiler, der som regel virker udmærket til engelsk tale. Nu kan man give dette nyoprettede Decoder-objekt >WAV-filer med data, der skal bearbejdes. Som du erindrer, har vi tidligere gemt den optagede tale som WAV-fil. Når du skal få denne lyd optaget i det rigtige format, skal du redigere koden fra første trin og sikre dig, at du optager i mono (ved for eksempel at bruge én kanal) og optager ved 16 kHz med 16 bit-kvalitet. For at læse den rigtigt kan du bruge et filobjekt og indlæse det som binær fil med læserettigheder. WAV-filer har en stump header-data i begyndelsen, og den skal du springe over.

Det gør du ved at bruge funktionen seek til at springe over de første 44 bytes. Nu er din filpointer i den korrekte position, og du kan videregive filobjektet til Decoder-objektets decode_raw() -function. Nu foretager det en masse dataknusning for at prøve at finde ud af, hvad der er blevet sagt. For at få resultaterne, skal du bruge funktionskaldet get_hyp() . Du får nu en liste med tre elementer fra denne funktion: en streng, der indeholder det bedste gæt på en talt tekst, en streng, der indeholder ytrings-id og et tal, der indeholder scoren for dette gæt.

>> Indtil nu har vi set på, hvordan man bruger det generiske sprog og audiomodeller til et bestemt sprog. Men Pocket Sphinx er et sprogsystem på forskningsniveau, og det har derfor redskaber, som du kan bruge til at bygge dine egne modeller [Billede  A]. På denne måde kan du træne din kode til at forstå din egen stemme med alle dens særpræg og betoninger. Det er en langvarig proces, og det er kun de færreste, der orker at gøre noget så intensivt. Men hvis du er interesseret, kan du finde information på det centrale website (http://cmusphinx.sourceforge.net). Du kan også definere dine egne modeller og grammatik og dermed fortælle Pocket Sphinx, hvordan det skal tolke de lyde, det arbejder med. Hvis du vil løse disse opgaver, kræver det en hel del læsning af dig.

>> Hvis du vil arbejde mere direkte med audio, kan du bede Pocket Sphinx om at begynde processen med funktionen
start_utt() . Så kan du begynde at læse audio fra din mikrofon. Du er nødt til at indlæse datablokke af passende størrelse, før du giver dem til Pocket Sphinx – specifikt til funktionen process_raw() – og du skal stadig bruge funktionen get_hyp() for at få fat i den oversatte tekst. Og fordi din kode ikke kan vide, når nogle har afsluttet en komplet ytring, skal man gøre det fra en løkke. Ved hver af løkkens omgange skal du læse en anden stump audio og videregive den til Pocket Sphinx. Dernæst skal du kalde get_hyp() igen for at se, om du kan få noget forståeligt ud af dine data. Når du er færdig med dette realtids-arbejde, kan du bruge funktionen end_utt() .

udliciter opgaver: komplet kodeoversigt

# Først skal man importere de nødvendige moduler
import pocketsphinx as ps
import sphinxbase

# Dernæst skal du oprette et Decoder-objekt
hmmd = ‘/usr/share/pocketsphinx/model/hmm/wsj1’
lmd = ‘/usr/share/pocketsphinx/lm/wsj/wlist5o.3e-7.vp.tg.lm.DMP’
dictd = ‘/usr/share/pocketsphinx/lm/wsj/wlist5o.dic’
d = ps.Decoder(hmm=hmmd, lm=lmd, dict=dictd)

# Du skal springe over header.informationen i din WAV-fil
wavFile = file(‘my_file.wav’, ‘rb’)
wavFile.seek(44)

# Nu kan du afkode din audio
d.decode_raw(wavFile)
results = d.get_hyp()

# Det mest oplagte gæt er det første
decoded_speech = results[0]
print “I said “, decoded_speech[0], “ with a confidence of ”, decoded_speech[1]

# Før du laver live-afkodning, skal du have PyAudio-modulet
import pyaudio
p = pyaudio.PyAudio()

# Nu kan du åbne en input-stream
in_stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000,input=True, frames_per_buffer=1024)
in_stream.start_stream()

# Nu kan du begynde at afkode
d.start_utt()
while True:
buf = in_stream.read(1024)
d.process_raw(buf, False, False)
results = d.get_hyp()
# Her vil du gøre noget, der bygger på den afkodede tale
# Når du er færdig, kan du lukke for det hele
break
d.end_utt()

3 Sociale medier

Du vil måske gerne lade dit system tjekke dine sociale medie-konti på internettet. Der findes adskillige Python-moduler, som kan klare det. Lad os antage, at du vil kunne tjekke din Facebook-konto. Installer det følgende Python-modul:

sudo apt-get install python-facebook

>> Du kan bruge import facebook til at få adgang til Facebooks API. Hvis du er Twitter-bruger, kan du installere Debians python-twitter-pakke og få adgang til Twitter-API’et. E-mail er nemmere, forudsat at din e-mailudbyder leverer IMAP- eller POP-adgang. Du kan importere e-mails og få stemmestyring til at læse ulæste e-mails op for dig. Til Google-fans har Google et Python-modul, der giver adgang til API’erne til stort set alting: Du kan arbejde med din kalender, e-mail eller med fitnessdata.

>> Nu burde du have en streng, der indeholder den tekst, som blev udtalt til din Raspberry Pi. Men du skal finde ud af, hvilken kommando det fører til. En metode består i at søge efter nøgleord. Hvis du har adgang til en liste over nøgleord, kan du køre igennem dem og søge efter hovedstrengen for at se, om nogen af disse nøgleord eksisterer i den som en understreng. Så kan du eksekvere den tilknyttede opgave med dette nøgleord. Denne metode finder imidlertid kun det første match. Hvad sker der, hvis din bruger kommer til at inddrage et nøgleord i sin talte kommando før det egentlige kommando-ord? Det er den auditive ækvivalent til at have tykke fingre og skrive en fejl i en kommando på tastaturet. Der foregår til stadighed et forskningsarbejde med henblik på at håndtere disse fejl på en fornuftig måde. Det kan være, at du kan lave en ny algoritme til at håndtere den slags situationer. Giv os et praj, hvis du finder på noget.

>> Lad os sige, at du har en række Python-scripts, der indeholder de forskellige opgaver, du vil have dit system til at klare. Du skal bruge en metode til at få disse scripts til at køre, når du beder om det. Den mest direkte måde at køre et script på består i at bruge execfile . Vi antager, at du har et script ved navn “do_task.py”, der indeholder Pythonkode, og som du gerne vil have kørt, når der bliver givet en kommando. Det kan du køre med
execfile(“do_task.py”)

>> Med denne form kan du tilføje kommandolinjefunktioner til den streng, der bliver indlæst. Den leder i den aktuelle mappe efter det script, der svarer til et filnavn, og kører det i dit hovedprograms aktuelle eksekveringskontekst. Hvis du har brug for at køre denne kode flere gange, skal du kalde execfile hver gang. Hvis du ikke ønsker, at scriptet kører i den samme kontekst, skal du bruge Subprocess-modulet. Det kan du importere med:
import subprocess

>> Derefter kan du eksekvere scriptet således:
subprocess.call(“do_task.py”)

>> Dette aktiverer en underproces af den centrale Python-oversætter og kører dit script der. Hvis dit script skal interagere med hovedprogrammet, er dette nok ikke den rette metode. Det er ikke enkelt at samle output fra et kald til “do_task.py” med Subprocess, og en bedre måde at opnå det samme på består i at bruge import -statementet. Det kører også koden i dit script på det punkt, hvor import -statementet bliver kaldt. Hvis dit script kun indeholder eksekverbare Python-statements, bliver de kørt under importen. Når du vil genkøre denne kode, skal du bruge kommandoen reload . Kommandoen reload eksisterer ikke i version tre af Python – hvis du bruger netop den Python-version, er det bedre at indkapsle den kode, der er indeholdt i scriptet i en funktion. Så kan du importere scriptet ved begyndelsen af dit hovedprogram og simpelthen kalde den relevante funktion på det rigtige tidspunkt. Det er en langt mere Python-agtig metode at bruge. Hvis du har det følgende indhold for

do_task.py…
def do_func():
do_task1()
do_task2()

… kan du bruge det med den følgende kode i dit hovedprogram:

import do_task
....
....
do_task.do_func()
....

>> En endnu mere Python-agtig metode består i at bruge klasser og objekter. Man kan skrive et script, som definerer en klasse, der indeholder metoder, som du kan kalde, når du har brug for det.

>> Hvad er dine muligheder, hvis du vil gøre noget, som man ikke kan opnå med et Python-script? I disse tilfælde skal du kunne køre arbitrære programmer på værtssystemet. Værtssystemet er i dette tilfælde din Raspberry Pi. Vi kan for eksempel sige, at du har brug for at downloade nogle e-mails ved hjælp af programmet Fetchmail. Det kan du gøre på et par forskellige måder. Den ældste metode består i at bruge kommandoen os.system() , når du indlæser en streng. I vores eksempel vil det se mere eller mindre sådan ud:
os.system(“/usr/bin/fetchmail”)

>> Du skal eksplicit bruge os.wait() for at få at vide, nøjagtig hvornår opgaven er afsluttet. Denne metode bliver nu erstattet af det nyere Subprocess-modul. Det giver dig mere kontrol over, hvordan opgaven bliver kørt, og hvordan du kan interagere med den. En simpel ækvivalent til den ovenstående kommando ville se sådan ud:
subprocess.call(“/usr/bin/fetchmail”)

>> Den venter, indtil det kaldte program er færdigt, og vender så tilbage til din centrale Python-proces. Men hvad hvis dit eksterne program skal indlæse resultater til dit hovedprogram? I så fald kan du bruge kommandoen

subprocess.check_output() . Det er i princippet det samme som subprocess.call() , bortset fra at når det slutter, bliver alt det, der er skrevet ud af det eksterne program til stdout, håndteret som et streng-object. Hvis du også ønsker information skrevet ud på stderr, kan du tilføje parameteren stderr=subprocess.STDOUT til dit kald til subprocess.check_output .

>> Nu burde du været så velorienteret i det grundlæggende, at du kan bygge din egen version af J.A.R.V.I.S.-systemet. Du kan finjustere den og få den til at gøre stort set alt, hvad du beder den om. Klø på, og sæt dine maskiner i gang. Få dem til faktisk at høre på, hvad du siger – for en gangs skyld.

Sociale medier: kompletkodeoversigt

do_task.py
----------
def do_func():
print “Hello World”

main_program.py
---------------

# Du kan lade dit eget modul udføre opgaver
import do_task

# Så kan du køre inkluderede funktioner
do_task.do_func()

# Du kan køre systemprogrammer direkte
import os

# Exitkoden fra dit program er i den variable returkode
returncode = os.system(“/usr/bin/fetchmail”)

# Subproces-modulet er et bedre valg
import subprocess

# Du kan duplikere ovenstående med
returncode = subprocess.call(“/usr/bin/fetchmail”)

# Hvis du også vil have outputtet, kan du bruge
returned_data = subprocess.check_output(“/usr/bin/
fetchmail”)