summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorschneefux <schneefux+commit@schneefux.xyz>2014-10-01 14:32:11 +0200
committerschneefux <schneefux+commit@schneefux.xyz>2014-10-01 14:36:20 +0200
commit1a833f30fe98acc4692b4f8684a6d1f9baa99d82 (patch)
treed0f31d8c6a1559229698d3688577ff766431ffe4 /client
parentd378caee59df005e94ba6f891976ceb64c2a48d2 (diff)
downloadjasper-client-1a833f30fe98acc4692b4f8684a6d1f9baa99d82.tar.gz
jasper-client-1a833f30fe98acc4692b4f8684a6d1f9baa99d82.zip
Readd MusicMode as dedicated module
Diffstat (limited to 'client')
-rw-r--r--client/modules/MPDControl.py428
1 files changed, 428 insertions, 0 deletions
diff --git a/client/modules/MPDControl.py b/client/modules/MPDControl.py
new file mode 100644
index 0000000..ecf857e
--- /dev/null
+++ b/client/modules/MPDControl.py
@@ -0,0 +1,428 @@
+# -*- coding: utf-8-*-
+import os
+import re
+import logging
+import difflib
+import mpd
+import g2p
+import stt
+from mic import Mic
+
+# Standard module stuff
+WORDS = ["MUSIC", "SPOTIFY"]
+
+def handle(text, mic, profile):
+ """
+ Responds to user-input, typically speech text, by telling a joke.
+
+ Arguments:
+ text -- user-input, typically transcribed speech
+ mic -- used to interact with the user (for both input and output)
+ profile -- contains information related to the user (e.g., phone number)
+ """
+ logger = logging.getLogger(__name__)
+
+ kwargs = {}
+ if 'mpdclient' in profile:
+ if 'server' in profile['mpdclient']:
+ kwargs['server'] = profile['mpdclient']['server']
+ if 'port' in profile['mpdclient']:
+ kwargs['port'] = int(profile['mpdclient']['port'])
+
+ logger.debug("Preparing to start music module")
+ try:
+ mpdwrapper = MPDWrapper(**kwargs)
+ except:
+ logger.error("Couldn't connect to MPD server", exc_info=True)
+ mic.say("I'm sorry. It seems that Spotify is not enabled. Please read the documentation to learn how to configure Spotify.")
+ return
+
+ mic.say("Please give me a moment, I'm loading your Spotify playlists.")
+
+ # FIXME: Make this configurable
+ persona = 'JASPER'
+
+ logger.debug("Starting music mode")
+ music_mode = MusicMode(persona, mic, mpdwrapper)
+ music_mode.handleForever()
+ logger.debug("Exiting music mode")
+
+ return
+
+def isValid(text):
+ """
+ Returns True if the input is related to jokes/humor.
+
+ Arguments:
+ text -- user-input, typically transcribed speech
+ """
+ return any(word in text.upper() for word in WORDS)
+
+# The interesting part
+class MusicMode(object):
+
+ def __init__(self, PERSONA, mic, mpdwrapper):
+ self.persona = PERSONA
+ # self.mic - we're actually going to ignore the mic they passed in
+ self.music = mpdwrapper
+
+ # index spotify playlists into new dictionary and language models
+ original = self.music.get_soup_playlist(
+ ) + ["STOP", "CLOSE", "PLAY", "PAUSE",
+ "NEXT", "PREVIOUS", "LOUDER", "SOFTER", "LOWER", "HIGHER", "VOLUME", "PLAYLIST"]
+ pronounced = g2p.translateWords(original)
+ zipped = zip(original, pronounced)
+ lines = ["%s %s" % (x, y) for x, y in zipped]
+
+ with open("dictionary_spotify.dic", "w") as f:
+ f.write("\n".join(lines) + "\n")
+
+ with open("sentences_spotify.txt", "w") as f:
+ f.write("\n".join(original) + "\n")
+ f.write("<s> \n </s> \n")
+ f.close()
+
+ # make language model
+ os.system(
+ "text2idngram -vocab sentences_spotify.txt < sentences_spotify.txt -idngram spotify.idngram")
+ os.system(
+ "idngram2lm -idngram spotify.idngram -vocab sentences_spotify.txt -arpa languagemodel_spotify.lm")
+
+ # create a new mic with the new music models
+ self.mic = Mic(
+ mic.speaker,
+ mic.passive_stt_engine,
+ stt.PocketSphinxSTT(lmd_music="languagemodel_spotify.lm", dictd_music="dictionary_spotify.dic")
+ )
+
+ def delegateInput(self, input):
+
+ command = input.upper()
+
+ # check if input is meant to start the music module
+ if "PLAYLIST" in command:
+ command = command.replace("PLAYLIST", "")
+ elif "STOP" in command:
+ self.mic.say("Stopping music")
+ self.music.stop()
+ return
+ elif "PLAY" in command:
+ self.mic.say("Playing %s" % self.music.current_song())
+ self.music.play()
+ return
+ elif "PAUSE" in command:
+ self.mic.say("Pausing music")
+ # not pause because would need a way to keep track of pause/play
+ # state
+ self.music.stop()
+ return
+ elif any(ext in command for ext in ["LOUDER", "HIGHER"]):
+ self.mic.say("Louder")
+ self.music.volume(interval=10)
+ self.music.play()
+ return
+ elif any(ext in command for ext in ["SOFTER", "LOWER"]):
+ self.mic.say("Softer")
+ self.music.volume(interval=-10)
+ self.music.play()
+ return
+ elif "NEXT" in command:
+ self.mic.say("Next song")
+ self.music.play() # backwards necessary to get mopidy to work
+ self.music.next()
+ self.mic.say("Playing %s" % self.music.current_song())
+ return
+ elif "PREVIOUS" in command:
+ self.mic.say("Previous song")
+ self.music.play() # backwards necessary to get mopidy to work
+ self.music.previous()
+ self.mic.say("Playing %s" % self.music.current_song())
+ return
+
+ # SONG SELECTION... requires long-loading dictionary and language model
+ # songs = self.music.fuzzy_songs(query = command.replace("PLAY", ""))
+ # if songs:
+ # self.mic.say("Found songs")
+ # self.music.play(songs = songs)
+
+ # print "SONG RESULTS"
+ # print "============"
+ # for song in songs:
+ # print "Song: %s Artist: %s" % (song.title, song.artist)
+
+ # self.mic.say("Playing %s" % self.music.current_song())
+
+ # else:
+ # self.mic.say("No songs found. Resuming current song.")
+ # self.music.play()
+
+ # PLAYLIST SELECTION
+ playlists = self.music.fuzzy_playlists(query=command)
+ if playlists:
+ self.mic.say("Loading playlist %s" % playlists[0])
+ self.music.play(playlist_name=playlists[0])
+ self.mic.say("Playing %s" % self.music.current_song())
+ else:
+ self.mic.say("No playlists found. Resuming current song.")
+ self.music.play()
+
+ return
+
+ def handleForever(self):
+
+ self.music.play()
+ self.mic.say("Playing %s" % self.music.current_song())
+
+ while True:
+
+ try:
+ threshold, transcribed = self.mic.passiveListen(self.persona)
+ except:
+ continue
+
+ if threshold:
+
+ self.music.pause()
+
+ input = self.mic.activeListen(MUSIC=True)
+
+ if "close" in input.lower():
+ self.mic.say("Closing Spotify")
+ return
+
+ if input:
+ self.delegateInput(input)
+ else:
+ self.mic.say("Pardon?")
+ self.music.play()
+
+def reconnect(func, *default_args, **default_kwargs):
+ """
+ Reconnects before running
+ """
+
+ def wrap(self, *default_args, **default_kwargs):
+ try:
+ self.client.connect(self.server, self.port)
+ except:
+ pass
+
+ # sometimes not enough to just connect
+ try:
+ return func(self, *default_args, **default_kwargs)
+ except:
+ self.client = mpd.MPDClient()
+ self.client.timeout = None
+ self.client.idletimeout = None
+ self.client.connect(self.server, self.port)
+
+ return func(self, *default_args, **default_kwargs)
+
+ return wrap
+
+
+class Song(object):
+ def __init__(self, id, title, artist, album):
+
+ self.id = id
+ self.title = title
+ self.artist = artist
+ self.album = album
+
+class MPDWrapper(object):
+
+ client = None
+ songs = [] # may have duplicates
+ playlists = []
+
+ # capitalized strings
+ song_titles = []
+ song_artists = []
+
+ def __init__(self, server="localhost", port=6600):
+ """
+ Prepare the client and music variables
+ """
+ self.server = server
+ self.port = port
+
+ # prepare client
+ self.client = mpd.MPDClient()
+ self.client.timeout = None
+ self.client.idletimeout = None
+ self.client.connect(self.server, self.port)
+
+ # gather playlists
+ self.playlists = [x["playlist"] for x in self.client.listplaylists()]
+
+ # gather songs
+ self.client.clear()
+ for playlist in self.playlists:
+ self.client.load(playlist)
+
+ soup = self.client.playlist()
+ for i in range(0, len(soup) / 10):
+ index = i * 10
+ id = soup[index].strip()
+ title = soup[index + 3].strip().upper()
+ artist = soup[index + 2].strip().upper()
+ album = soup[index + 4].strip().upper()
+
+ self.songs.append(Song(id, title, artist, album))
+
+ self.song_titles.append(title)
+ self.song_artists.append(artist)
+
+ @reconnect
+ def play(self, songs=False, playlist_name=False):
+ """
+ Plays the current song or accepts a song to play.
+
+ Arguments:
+ songs -- a list of song objects
+ playlist_name -- user-defined, something like "Love Song Playlist"
+ """
+ if songs:
+ self.client.clear()
+ for song in songs:
+ try: # for some reason, certain ids don't work
+ self.client.add(song.id)
+ except:
+ pass
+
+ if playlist_name:
+ self.client.clear()
+ self.client.load(playlist_name)
+
+ self.client.play()
+
+ @reconnect
+ def current_song(self):
+ item = self.client.playlistinfo(int(self.client.status()["song"]))[0]
+ result = "%s by %s" % (item["title"], item["artist"])
+ return result
+
+ @reconnect
+ def volume(self, level=None, interval=None):
+
+ if level:
+ self.client.setvol(int(level))
+ return
+
+ if interval:
+ level = int(self.client.status()['volume']) + int(interval)
+ self.client.setvol(int(level))
+ return
+
+ @reconnect
+ def pause(self):
+ self.client.pause()
+
+ @reconnect
+ def stop(self):
+ self.client.stop()
+
+ @reconnect
+ def next(self):
+ self.client.next()
+ return
+
+ @reconnect
+ def previous(self):
+ self.client.previous()
+ return
+
+ def get_soup(self):
+ """
+ returns the list of unique words that comprise song and artist titles
+ """
+
+ soup = []
+
+ for song in self.songs:
+ song_words = song.title.split(" ")
+ artist_words = song.artist.split(" ")
+ soup.extend(song_words)
+ soup.extend(artist_words)
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", "")
+ for x in soup]
+ soup = [x for x in soup if x != ""]
+
+ return list(set(soup))
+
+ def get_soup_playlist(self):
+ """
+ returns the list of unique words that comprise playlist names
+ """
+
+ soup = []
+
+ for name in self.playlists:
+ soup.extend(name.split(" "))
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", "")
+ for x in soup]
+ soup = [x for x in soup if x != ""]
+
+ return list(set(soup))
+
+ def get_soup_separated(self):
+ """
+ returns the list of PHRASES that comprise song and artist titles
+ """
+
+ title_soup = [song.title for song in self.songs]
+ artist_soup = [song.artist for song in self.songs]
+
+ soup = list(set(title_soup + artist_soup))
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", " ")
+ for x in soup]
+ soup = [re.sub(' +', ' ', x) for x in soup if x != ""]
+
+ return soup
+
+ def fuzzy_songs(self, query):
+ """
+ Returns songs matching a query best as possible on either artist field, etc
+ """
+
+ query = query.upper()
+
+ matched_song_titles = difflib.get_close_matches(query, self.song_titles)
+ matched_song_artists = difflib.get_close_matches(query, self.song_artists)
+
+ # if query is beautifully matched, then forget about everything else
+ strict_priority_title = [x for x in matched_song_titles if x == query]
+ strict_priority_artists = [
+ x for x in matched_song_artists if x == query]
+
+ if strict_priority_title:
+ matched_song_titles = strict_priority_title
+ if strict_priority_artists:
+ matched_song_artists = strict_priority_artists
+
+ matched_songs_bytitle = [
+ song for song in self.songs if song.title in matched_song_titles]
+ matched_songs_byartist = [
+ song for song in self.songs if song.artist in matched_song_artists]
+
+ matches = list(set(matched_songs_bytitle + matched_songs_byartist))
+
+ return matches
+
+ def fuzzy_playlists(self, query):
+ """
+ returns playlist names that match query best as possible
+ """
+ query = query.upper()
+ lookup = {n.upper(): n for n in self.playlists}
+ results = [lookup[r] for r in difflib.get_close_matches(query, lookup)]
+ return results \ No newline at end of file