Permalink
Browse files

resolves #1, announced documented and new feature to playback files i…

…f a mapping exists.
  • Loading branch information...
1 parent ff7ac4f commit 0bd45bd03d8c0db2ec4876cc393f0b72679f37db @eastein committed Apr 15, 2012
Showing with 100 additions and 0 deletions.
  1. +20 −0 README.md
  2. +19 −0 README.txt
  3. +61 −0 announced
View
@@ -2,3 +2,23 @@
# Mission: Announcable
Voiceovers. Informative soundbites. Say it with emphasis and a somewhat below-par text to speech engine.
+
+<A name="toc1-5" title="Networked Announcer Daemon: announced" />
+# Networked Announcer Daemon: announced
+
+The announce daemon, **announced**, takes 2 arguments: one, a URL to bind to for ZeroMQ SUB, and optionally a filename to a json file full of text to filename mappings. For example
+
+ [
+ ["hello world", "helloworld.mp3"],
+ ["flee for your lives, puny mortals.", "flee.mp3"]
+ ]
+
+In this case, if those files are in the working directory from which **announced** was invoked, if strings matching (or close-enough, read the code if you care how the close-enough logic works, it's pretty simple) the first part of each mapping are sent, then the text to speech engine will be skipped and the files will be played instead.
+
+Messages sent to the **announced** daemon have 2 required arguments: `text` and `pitch`. Pitch is currently not used when a file is matched.
+
+ import zmqsub
+ spk = zmqsub.JSONZMQConnectPub('tcp://*:4900')
+ spk.send({'text': 'flee for your lives, puny mortals', 'pitch' : 0})
+
+The above example would cause the `flee.mp3` file to be played back.
View
@@ -1,3 +1,22 @@
# Mission: Announcable
Voiceovers. Informative soundbites. Say it with emphasis and a somewhat below-par text to speech engine.
+
+# Networked Announcer Daemon: announced
+
+The announce daemon, **announced**, takes 2 arguments: one, a URL to bind to for ZeroMQ SUB, and optionally a filename to a json file full of text to filename mappings. For example
+
+ [
+ ["hello world", "helloworld.mp3"],
+ ["flee for your lives, puny mortals.", "flee.mp3"]
+ ]
+
+In this case, if those files are in the working directory from which **announced** was invoked, if strings matching (or close-enough, read the code if you care how the close-enough logic works, it's pretty simple) the first part of each mapping are sent, then the text to speech engine will be skipped and the files will be played instead.
+
+Messages sent to the **announced** daemon have 2 required arguments: `text` and `pitch`. Pitch is currently not used when a file is matched.
+
+ import zmqsub
+ spk = zmqsub.JSONZMQConnectPub('tcp://*:4900')
+ spk.send({'text': 'flee for your lives, puny mortals', 'pitch' : 0})
+
+The above example would cause the `flee.mp3` file to be played back.
View
@@ -2,17 +2,78 @@
import time
import sys
+import json
import zmqsub
import subprocess
+# By Magnus Hetland (http://hetland.org/), props!
+def levenshtein(a,b):
+ "Calculates the Levenshtein distance between a and b."
+ n, m = len(a), len(b)
+ if n > m:
+ # Make sure n <= m, to use O(min(n,m)) space
+ a,b = b,a
+ n,m = m,n
+
+ current = range(n+1)
+ for i in range(1,m+1):
+ previous, current = current, [i]+[0]*n
+ for j in range(1,n+1):
+ add, delete = previous[j]+1, current[j-1]+1
+ change = previous[j-1]
+ if a[j-1] != b[i-1]:
+ change = change + 1
+ current[j] = min(add, delete, change)
+
+ return current[n]
+
+class RecordingSet(object) :
+ def __init__(self, recordings) :
+ self.recordings = recordings
+ self.toss_threshold_count = 10
+ self.toss_threshold_ratio = .2
+
+ def play(self, text) :
+ f = None
+ l = 1000000 # use maxint?
+
+ for t,fn in self.recordings :
+ lev = levenshtein(t, text)
+ ratio = lev * 1.0 / len(t)
+ if lev > self.toss_threshold_count :
+ continue
+ if ratio > self.toss_threshold_ratio :
+ continue
+
+ if lev < l :
+ f = fn
+ continue
+
+ if f is not None :
+ proc = subprocess.Popen(['mplayer', f], bufsize=1048576, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = proc.communicate()
+ proc.wait()
+ return True
+
+ return False
+
if __name__ == '__main__' :
sub = zmqsub.JSONZMQPub(sys.argv[1])
+ try :
+ recordings = json.load(open(sys.argv[2]))
+ except IndexError :
+ recordings = []
+
+ rs = RecordingSet(recordings)
while True :
try :
msg = sub.recv()
print 'recvd message %s' % msg
if 'text' in msg and 'pitch' in msg :
+ if rs.play(msg['text']) :
+ continue
+
proc = subprocess.Popen(['bash', 'saypitchprase', msg['text'].encode('ascii', 'ignore'), str(msg['pitch'])], bufsize=1048576, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
proc.wait()

0 comments on commit 0bd45bd

Please sign in to comment.