-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 6ee3dbb
Showing
5 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
A text editor with no visual output whatsoever. A text-to-speech engine reads | ||
edits aloud, and an auditory bell provides navigational feedback. | ||
|
||
Usage resembles TextEdit with VoiceOver enabled on OS X. This, however, is | ||
implemented in just a couple hundred lines of Python; plus, it's | ||
cross-platform, thanks to the [pyttsx](http://pypi.python.org/pypi/pyttsx) | ||
and [pyfluidsynth](http://code.google.com/p/pyfluidsynth/) libraries. | ||
|
||
Pre-alpha stability. BSD licensed. | ||
|
||
--- | ||
Daniel W. Steinbrook <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
#!/usr/bin/env python | ||
|
||
import curses | ||
import time | ||
|
||
import fluidsynth | ||
import pyttsx | ||
|
||
|
||
class AudioApp(object): | ||
'''Base class for auditory UI.''' | ||
def __init__(self, args): | ||
# initialize text-to-speech | ||
self.narrator = pyttsx.init() | ||
#self.narrator.setProperty('voice', 'english-us') # espeak | ||
self.narrator.setProperty('rate', 500) | ||
|
||
# initialize audio synthesizer | ||
self.synth = fluidsynth.Synth() | ||
self.synth.start() | ||
|
||
example = self.synth.sfload('example.sf2') | ||
self.synth.program_select(0, example, 0, 0) | ||
|
||
def speak(self, phrase): | ||
self.narrator.say(phrase) | ||
self.narrator.runAndWait() | ||
|
||
def speak_char(self, char): | ||
# TTS engine might not know how to pronounce these characters | ||
mapping = { | ||
'y': 'why', | ||
'.': 'dot', | ||
' ': 'space', | ||
',': 'comma', | ||
';': 'semicolon', | ||
'-': 'dash', | ||
':': 'colon', | ||
'/': 'slash', | ||
'\\': 'backslash', | ||
'?': 'question mark', | ||
'!': 'bang', | ||
'@': 'at', | ||
'#': 'pound', | ||
'$': 'dollar', | ||
'%': 'percent', | ||
'*': 'star', | ||
'^': 'caret', | ||
'~': 'squiggle' | ||
} | ||
speakable = char.lower() | ||
|
||
if speakable in mapping: | ||
speakable = mapping[speakable] | ||
elif char.isalpha(): | ||
speakable = char | ||
else: | ||
speakable = 'splork' # say something better | ||
|
||
return self.speak(speakable) | ||
|
||
def play_interval(self, size, duration, root=80, delay=0, intensity=30, channel=0): | ||
self.synth.noteon(channel, root, intensity) | ||
time.sleep(delay) | ||
self.synth.noteon(channel, root + size, intensity) | ||
time.sleep(duration) | ||
self.synth.noteoff(channel, root) | ||
self.synth.noteoff(channel, root + size) | ||
|
||
def handle_key(self, key): | ||
'''Individual apps (subclasses) must override this method.''' | ||
raise NotImplementedError | ||
|
||
def run(self): | ||
'''Loop indefinitely, passing keystrokes to handle_key until that | ||
method returns false.''' | ||
try: | ||
stdscr = curses.initscr() | ||
curses.noecho() | ||
curses.cbreak() | ||
stdscr.keypad(1) | ||
|
||
while True: | ||
c = stdscr.getch() | ||
if not self.handle_key(c): break | ||
|
||
finally: | ||
curses.nocbreak() | ||
stdscr.keypad(0) | ||
curses.echo() | ||
curses.endwin() | ||
|
||
# for debugging | ||
def simulate_keystroke(self, key): | ||
#time.sleep(random.uniform(0.01, 0.05)) | ||
self.handle_key(key) | ||
|
||
def simulate_typing(self, string): | ||
for char in string: | ||
self.simulate_keystroke(ord(char)) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
#!/usr/bin/env python | ||
|
||
import curses | ||
import curses.ascii | ||
import random | ||
import string | ||
import tempfile | ||
import unittest | ||
|
||
from text_editor import TextEditor | ||
|
||
|
||
class TextEditorTest(unittest.TestCase): | ||
def setUp(self): | ||
self.outfile = tempfile.NamedTemporaryFile() | ||
self.args = ['text_editor.py', self.outfile.name] | ||
self.app = TextEditor(self.args) | ||
|
||
# Override the sound-generating methods with silent recordkeeping. | ||
self.utterances = [] | ||
def instead_of_sounds(phrase, a=0, b=0, c=0, d=0, e=0): | ||
if type(phrase) is str: | ||
self.utterances.append(phrase) | ||
else: | ||
self.utterances.append('[tone]') | ||
self.app.speak = instead_of_sounds | ||
self.app.play_interval = instead_of_sounds | ||
|
||
def tearDown(self): | ||
#print repr(self.utterances) | ||
pass | ||
|
||
def test_simple(self): | ||
# should speak previous word after non-alpha character | ||
self.app.simulate_typing('hello world ') | ||
self.assertEqual(self.utterances[-1], 'world') | ||
|
||
# should speak character just deleted | ||
self.app.simulate_keystroke(curses.ascii.DEL) | ||
self.assertEqual(self.utterances[-1], 'space') | ||
|
||
# should play tone when attempting to move cursor beyond bounds | ||
self.app.simulate_keystroke(curses.KEY_RIGHT) | ||
self.assertEqual(self.utterances[-1], '[tone]') | ||
|
||
for i in range(5): # move cursor five characters left | ||
self.app.simulate_keystroke(curses.KEY_LEFT) | ||
self.app.simulate_typing('awesome ') | ||
|
||
self.app.simulate_keystroke(curses.ascii.ESC) # quit | ||
self.outfile.seek(0) | ||
|
||
# check that contents were properly saved | ||
self.assertEqual(self.outfile.read(), "hello awesome world") | ||
|
||
def test_symbols(self): | ||
self.app.simulate_typing('foo') | ||
self.app.simulate_keystroke(curses.ascii.ESC) # quit | ||
|
||
self.app = TextEditor(self.args) # re-open | ||
self.app.simulate_typing('~ ~') | ||
self.app.simulate_keystroke(curses.ascii.ESC) # quit | ||
|
||
self.outfile.seek(0) | ||
self.assertEqual(self.outfile.read(), "~ ~foo") | ||
|
||
def test_gibberish(self): | ||
# type n random printable characters | ||
n = 100 | ||
self.app.simulate_typing( | ||
random.choice(string.printable) for i in range(n)) | ||
self.app.simulate_keystroke(curses.ascii.ESC) # quit | ||
self.outfile.seek(0) | ||
|
||
self.assertEqual(n, len(self.outfile.read())) | ||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
#!/usr/bin/env python | ||
|
||
import curses | ||
import curses.ascii | ||
import os | ||
import sys | ||
|
||
from audio_app import AudioApp | ||
|
||
|
||
class TextEditor(AudioApp): | ||
def __init__(self, args): | ||
self.buffer = '' | ||
self.cursor = 0 | ||
|
||
if len(args) > 1: | ||
path = args[1] | ||
if os.path.exists(path): | ||
f = open(path) | ||
self.buffer = f.read() | ||
f.close() | ||
self.fd = open(path, 'w') | ||
|
||
super(TextEditor, self).__init__(args) | ||
|
||
def move_left(self): | ||
if self.cursor > 0: | ||
self.cursor -= 1 | ||
self.speak_char(self.buffer[self.cursor]) | ||
return True | ||
else: | ||
self.play_interval(1, 0.1) | ||
return False | ||
|
||
def move_right(self): | ||
if self.cursor < len(self.buffer): | ||
self.speak_char(self.buffer[self.cursor]) | ||
self.cursor += 1 | ||
return True | ||
else: | ||
self.play_interval(1, 0.1) | ||
return False | ||
|
||
def insert_char(self, char): | ||
self.buffer = self.buffer[:self.cursor] + char + self.buffer[self.cursor:] | ||
self.cursor += 1 | ||
|
||
def remove_char(self): | ||
if self.cursor == 0: | ||
self.play_interval(1, 0.1) | ||
return False | ||
|
||
old_char = self.buffer[self.cursor - 1] | ||
self.buffer = self.buffer[:self.cursor - 1] + self.buffer[self.cursor:] | ||
self.cursor -= 1 | ||
return old_char | ||
|
||
def backspace(self): | ||
deleted = self.remove_char() | ||
if deleted: | ||
self.speak_char(deleted) | ||
|
||
def last_word(self, ending_at=None): | ||
if ending_at is None: | ||
ending_at = self.cursor - 1 | ||
|
||
if self.cursor < 2: | ||
return self.buffer[0] | ||
|
||
start = ending_at - 1 | ||
while self.buffer[start].isalpha() and start > 0: | ||
start -= 1 | ||
|
||
return self.buffer[start:ending_at + 1].strip() | ||
|
||
def close(self): | ||
if hasattr(self, 'fd'): | ||
self.fd.write(self.buffer) # write changes to disk | ||
self.fd.close() | ||
|
||
def handle_key(self, key): | ||
#sys.stderr.write(str(key)) | ||
|
||
if key == curses.ascii.ESC: | ||
self.close() | ||
return False # exit | ||
|
||
elif key == curses.KEY_LEFT: | ||
self.move_left() | ||
|
||
elif key == curses.KEY_RIGHT: | ||
self.move_right() | ||
|
||
elif key == curses.ascii.DEL: #curses.KEY_BACKSPACE: | ||
self.backspace() | ||
|
||
elif curses.ascii.isalpha(key): | ||
self.insert_char(chr(key)) | ||
|
||
elif curses.ascii.isprint(key) or curses.ascii.isspace(key): | ||
self.insert_char(chr(key)) | ||
self.speak(self.last_word()) | ||
#sys.stderr.write(self.buffer + '\r') | ||
|
||
return True # keep reading keystrokes | ||
|
||
|
||
if __name__ == '__main__': | ||
app = TextEditor(sys.argv) | ||
app.run() |