freespeech.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # FreeSpeech
  4. # Continuous realtime speech recognition and control via pocketsphinx
  5. # Copyright (c) 2013, 2014 Henry Kroll III, http://www.TheNerdShow.com
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. import pygtk
  19. pygtk.require('2.0')
  20. import gtk
  21. import pygst
  22. pygst.require('0.10')
  23. import gobject
  24. gobject.threads_init()
  25. import gst
  26. import subprocess
  27. import platform, os, shutil, sys, codecs
  28. import re
  29. import json
  30. from send_key import *
  31. """ global variables """
  32. appname = 'FreeSpeech'
  33. refdir = 'lm'
  34. # hmmm, where to put files? How about XDG_CONFIG_HOME?
  35. # This will work on most Linux
  36. if os.environ.has_key('XDG_CONFIG_HOME'):
  37. confhome = os.environ['XDG_CONFIG_HOME']
  38. confdir = os.path.join(confhome, appname)
  39. else:
  40. if os.environ.has_key('HOME'):
  41. confhome = os.path.join(os.environ['HOME'],".config")
  42. confdir = os.path.join(confhome, appname)
  43. else:
  44. confdir = refdir
  45. # reference files written by this application
  46. lang_ref= os.path.join(confdir, 'freespeech.ref.txt')
  47. vocab = os.path.join(confdir, 'freespeech.vocab')
  48. idngram = os.path.join(confdir, 'freespeech.idngram')
  49. arpa = os.path.join(confdir, 'freespeech.arpa')
  50. dmp = os.path.join(confdir, 'freespeech.dmp')
  51. cmdtext = os.path.join(confdir, 'freespeech.cmd.txt')
  52. cmdjson = os.path.join(confdir, 'freespeech.cmd.json')
  53. class freespeech(object):
  54. """GStreamer/PocketSphinx Continuous Speech Recognition"""
  55. def __init__(self):
  56. """Initialize a freespeech object"""
  57. # place to store the currently open file name, if any
  58. self.open_filename=''
  59. # create confdir if not exists
  60. if not os.access(confdir, os.R_OK):
  61. os.mkdir(confdir)
  62. # copy lang_ref to confdir if not exists
  63. if not os.access(lang_ref, os.R_OK):
  64. lang_ref_orig = os.path.join(refdir, 'freespeech.ref.txt')
  65. shutil.copy(lang_ref_orig, lang_ref)
  66. # initialize components
  67. self.init_gui()
  68. self.init_errmsg()
  69. self.init_prefs()
  70. self.init_file_chooser()
  71. self.init_gst()
  72. def init_gui(self):
  73. self.undo = [] # Say "Scratch that" or "Undo that"
  74. """Initialize the GUI components"""
  75. self.window = gtk.Window()
  76. # Change to executable's dir
  77. if os.path.dirname(sys.argv[0]):
  78. os.chdir(os.path.dirname(sys.argv[0]))
  79. self.icon = gtk.gdk.pixbuf_new_from_file(appname+".png")
  80. self.window.connect("delete-event", gtk.main_quit)
  81. self.window.set_default_size(400, 200)
  82. self.window.set_border_width(10)
  83. self.window.set_icon(self.icon)
  84. self.window.set_title(appname)
  85. vbox = gtk.VBox()
  86. hbox = gtk.HBox(homogeneous=True)
  87. self.textbuf = gtk.TextBuffer()
  88. self.text = gtk.TextView(self.textbuf)
  89. self.text.set_wrap_mode(gtk.WRAP_WORD)
  90. self.scroller = gtk.ScrolledWindow(None, None)
  91. self.scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  92. self.scroller.add(self.text)
  93. vbox.pack_start(self.scroller, True, True, 5)
  94. vbox.pack_end(hbox, False, False)
  95. self.button0 = gtk.Button("Learn")
  96. self.button0.connect('clicked', self.learn_new_words)
  97. self.button1 = gtk.ToggleButton("Send keys")
  98. self.button1.connect('clicked', self.toggle_echo)
  99. self.button2 = gtk.Button("Show commands")
  100. self.button2.connect('clicked', self.show_commands)
  101. self.button3 = gtk.ToggleButton("Mute")
  102. self.button3.connect('clicked', self.mute)
  103. hbox.pack_start(self.button0, True, False, 0)
  104. hbox.pack_start(self.button1, True, False, 0)
  105. hbox.pack_start(self.button2, True, False, 0)
  106. hbox.pack_start(self.button3, True, False, 0)
  107. self.window.add(vbox)
  108. self.window.show_all()
  109. def init_file_chooser(self):
  110. self.file_chooser = gtk.FileChooserDialog(title="File Chooser",
  111. parent=self.window, action=gtk.FILE_CHOOSER_ACTION_OPEN,
  112. buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
  113. gtk.STOCK_OK, gtk.RESPONSE_ACCEPT), backend=None)
  114. def init_commands(self):
  115. self.commands = {'file quit': 'gtk.main_quit',
  116. 'file open': 'self.file_open',
  117. 'file save': 'self.file_save',
  118. 'file save as': 'self.file_save_as',
  119. 'show commands': 'self.show_commands',
  120. 'editor clear': 'self.clear_edits',
  121. 'clear edits': 'self.clear_edits',
  122. 'file close': 'self.clear_edits',
  123. 'delete': 'self.delete',
  124. 'select': 'self.select',
  125. 'send keys' : 'self.toggle_keys',
  126. 'insert': 'self.insert',
  127. 'go to the end': 'self.done_editing',
  128. 'done editing': 'self.done_editing',
  129. 'scratch that': 'self.scratch_that',
  130. 'back space': 'self.backspace',
  131. 'new paragraph': 'self.new_paragraph',
  132. }
  133. self.write_prefs()
  134. try:
  135. self.prefsdialog.checkbox.set_active(False)
  136. except:
  137. pass
  138. def init_prefs(self):
  139. """Initialize new GUI components"""
  140. me = self.prefsdialog = gtk.Dialog("Command Preferences", None,
  141. gtk.DIALOG_DESTROY_WITH_PARENT,
  142. (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
  143. gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
  144. me.set_default_size(400, 300)
  145. if not os.access(cmdjson, os.R_OK):
  146. #~ write some default commands to a file if it doesn't exist
  147. self.init_commands()
  148. else:
  149. self.read_prefs()
  150. me.label = gtk.Label( \
  151. "Double-click to change command wording.\n\
  152. If new commands don't work click the learn button to train them.")
  153. me.vbox.pack_start(me.label)
  154. me.checkbox=gtk.CheckButton("Restore Defaults")
  155. me.checkbox.show()
  156. me.action_area.pack_start(me.checkbox)
  157. me.liststore=gtk.ListStore(str, str)
  158. me.liststore.set_sort_column_id(0, gtk.SORT_ASCENDING)
  159. me.tree=gtk.TreeView(me.liststore)
  160. editable = gtk.CellRendererText()
  161. fixed = gtk.CellRendererText()
  162. editable.set_property('editable', True)
  163. editable.connect('edited', self.edited_cb)
  164. me.connect("expose-event", self.prefs_expose)
  165. me.connect("response", self.prefs_response)
  166. column = gtk.TreeViewColumn("Spoken command",editable,text=0)
  167. me.tree.append_column(column)
  168. column = gtk.TreeViewColumn("What it does",fixed,text=1)
  169. me.tree.append_column(column)
  170. me.vbox.pack_end(me.tree)
  171. me.label.show()
  172. me.tree.show()
  173. self.commands_old = self.commands
  174. me.show_all()
  175. def prefs_expose(self, me, event):
  176. """ callback when prefs window is shown """
  177. # populate commands list with documentation
  178. me.liststore.clear()
  179. for x,y in self.commands.items():
  180. me.liststore.append([x,eval(y).__doc__])
  181. def write_prefs(self):
  182. """ write command list to file """
  183. with codecs.open(cmdjson, encoding='utf-8', mode='w') as f:
  184. f.write(json.dumps(self.commands))
  185. # write commands text, so we don't have to train each time
  186. with codecs.open(cmdtext, encoding='utf-8', mode='w') as f:
  187. for j in self.commands.keys():
  188. f.write('<s> '+j+' </s>\n')
  189. def read_prefs(self):
  190. """ read command list from file """
  191. with codecs.open(cmdjson, encoding='utf-8', mode='r') as f:
  192. self.commands=json.loads(f.read())
  193. def prefs_response(self, me, event):
  194. """ make prefs dialog non-modal by using response event
  195. instead of run() method, which waited for input """
  196. if me.checkbox.get_active():
  197. self.init_commands()
  198. else:
  199. if event!=gtk.RESPONSE_ACCEPT:
  200. self.commands = self.commands_old
  201. else:
  202. self.write_prefs()
  203. me.hide()
  204. def edited_cb(self, cellrenderertext, path, new_text):
  205. """ callback activated when treeview text edited """
  206. #~ self.prefsdialog.tree.path=new_text
  207. liststore=self.prefsdialog.liststore
  208. treeiter = liststore.get_iter(path)
  209. old_text = liststore.get_value(treeiter,0)
  210. if not self.commands.has_key(new_text):
  211. liststore.set_value(treeiter,0,new_text)
  212. self.commands[new_text]=self.commands[old_text]
  213. del(self.commands[old_text])
  214. #~ print(old_text, new_text)
  215. def init_errmsg(self):
  216. me = self.errmsg = gtk.Dialog("Error", None,
  217. gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
  218. (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
  219. gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
  220. me.set_default_size(400, 200)
  221. me.label = gtk.Label("Nice label")
  222. me.vbox.pack_start(me.label)
  223. me.label.show()
  224. def init_gst(self):
  225. """Initialize the speech components"""
  226. self.pipeline = gst.parse_launch('autoaudiosrc ! audioconvert ! audioresample '
  227. + '! vader name=vad auto-threshold=true '
  228. + '! pocketsphinx name=asr ! fakesink')
  229. asr = self.pipeline.get_by_name('asr')
  230. """Load custom dictionary and language model"""
  231. asr.set_property('dict', 'custom.dic')
  232. # The language model that came with pocketsphinx works OK...
  233. # asr.set_property('lm', '/usr/share/pocketsphinx/model/lm/en_US/wsj0vp.5000.DMP')
  234. # but it does not contain editing commands, so we make our own
  235. if not os.access(dmp, os.R_OK): # create if not exists
  236. self.learn_new_words(None)
  237. asr.set_property('lm', dmp)
  238. # Adapt pocketsphinx to your voice for better accuracy.
  239. # See http://cmusphinx.sourceforge.net/wiki/tutorialadapt
  240. # asr.set_property('hmm', '../sphinx/hub4wsj_sc_8kadapt')
  241. #fixme: write an acoustic model trainer
  242. asr.connect('partial_result', self.asr_partial_result)
  243. asr.connect('result', self.asr_result)
  244. asr.set_property('configured', True)
  245. bus = self.pipeline.get_bus()
  246. bus.add_signal_watch()
  247. bus.connect('message::application', self.application_message)
  248. #self.pipeline.set_state(gst.STATE_PAUSED)
  249. self.pipeline.set_state(gst.STATE_PLAYING)
  250. def learn_new_words(self, button):
  251. """ Learn new words, jargon, or other language
  252. 1. Add the word(s) to the dictionary, if necessary.
  253. 2. Type or paste sentences containing the word(s).
  254. 2. Use the word(s) differently in at least 3 sentences.
  255. 3. Click the "Learn" button. """
  256. # prepare a text corpus from the textbox
  257. corpus = self.prepare_corpus(self.textbuf)
  258. # append it to the language reference
  259. with codecs.open(lang_ref, encoding='utf-8', mode='a+') as f:
  260. for line in corpus:
  261. if line:
  262. f.write(line + '\n')
  263. # cat command
  264. if platform.system()=='Windows':
  265. catcmd = 'type '
  266. else:
  267. catcmd = 'cat '
  268. # compile a vocabulary
  269. # http://www.speech.cs.cmu.edu/SLM/toolkit_documentation.html#text2wfreq
  270. if subprocess.call(catcmd + cmdtext + ' ' + cmdtext + ' ' + cmdtext + ' ' + cmdtext + ' ' + lang_ref + '|text2wfreq -verbosity 2' \
  271. + ' |wfreq2vocab -top 20000 -records 100000 > ' + vocab, shell=True):
  272. self.err('Trouble writing ' + vocab)
  273. # update the idngram\
  274. # http://www.speech.cs.cmu.edu/SLM/toolkit_documentation.html#text2idngram
  275. if subprocess.call('text2idngram -vocab ' + vocab + \
  276. ' -n 3 < ' + lang_ref + ' > ' + idngram, shell=True):
  277. self.err('Trouble writing ' + idngram)
  278. # (re)build arpa language model
  279. # http://drupal.cs.grinnell.edu/~stone/courses/computational-linguistics/ngram-lab.html
  280. if subprocess.call('idngram2lm -idngram -n 3 -verbosity 2 ' + idngram + \
  281. ' -vocab ' + vocab + ' -arpa ' + arpa + ' -vocab_type 1' \
  282. ' -good_turing', shell=True):
  283. self.err('Trouble writing ' + arpa)
  284. # convert to dmp
  285. if subprocess.call('sphinx_lm_convert -i ' + arpa + \
  286. ' -o ' + dmp + ' -ofmt dmp', shell=True):
  287. self.err('Trouble writing ' + dmp)
  288. # load the dmp
  289. asr = self.pipeline.get_by_name('asr')
  290. self.pipeline.set_state(gst.STATE_PAUSED)
  291. asr.set_property('configured', False)
  292. asr.set_property('lm', dmp)
  293. asr.set_property('configured', True)
  294. self.pipeline.set_state(gst.STATE_PLAYING)
  295. def mute(self, button):
  296. """Handle button presses."""
  297. if not button.get_active():
  298. button.set_label("Mute")
  299. self.pipeline.set_state(gst.STATE_PLAYING)
  300. else:
  301. button.set_label("Speak")
  302. vader = self.pipeline.get_by_name('vad')
  303. vader.set_property('silent', True)
  304. self.pipeline.set_state(gst.STATE_PAUSED)
  305. def toggle_echo(self, button):
  306. """ echo keystrokes to the desktop """
  307. if not button.get_active():
  308. button.set_label("Send keys")
  309. button.set_active(False)
  310. else:
  311. button.set_label("Stop sending")
  312. button.set_active(True)
  313. def toggle_keys(self):
  314. """ echo keystrokes to the desktop """
  315. self.button1.set_active(True - self.button1.get_active())
  316. return True
  317. def collapse_punctuation(self, hyp, started):
  318. index = 0
  319. start = self.textbuf.get_iter_at_mark(self.textbuf.get_insert())
  320. words = hyp.split()
  321. # remove the extra text to the right of the punctuation mark
  322. while True:
  323. if (index >= len(words)):
  324. break
  325. word = words[index]
  326. if (re.match("^\W\w", word)):
  327. words[index] = word[0]
  328. index += 1
  329. hyp = " ".join(words)
  330. hyp = hyp.replace(" ...ellipsis", " ...")
  331. hyp = re.sub(r" ([^\w\s]+)\s*", r"\1 ", hyp)
  332. hyp = re.sub(r"([({[]) ", r" \1", hyp).strip()
  333. if not start.inside_sentence():
  334. hyp = hyp[0].capitalize() + hyp[1:]
  335. if re.match(r"\w", hyp[0]) and started:
  336. space = " "
  337. else:
  338. space = ""
  339. return space + hyp
  340. def expand_punctuation(self, corpus):
  341. # tweak punctuation to match dictionary utterances
  342. for ind, line in enumerate(corpus):
  343. line = re.sub(r'--', r'--dash', line)
  344. line = re.sub(r'- ', r'-hyphen ', line)
  345. line = re.sub(r'`', r'`agrave', line)
  346. line = re.sub(r'=', r'=equals-sign', line)
  347. line = re.sub(r'>', r'>greater-than-symbol', line)
  348. line = re.sub(r'<', r'<less-than-symbol', line)
  349. line = re.sub(r'\|', r'\|pipe-symbol', line)
  350. line = re.sub(r'\. \. \.', r'...ellipsis', line)
  351. line = re.sub(r' \. ', r' .dot ', line)
  352. line = re.sub(r'\.$', r'.period', line)
  353. line = re.sub(r',', r',comma', line)
  354. line = re.sub(r':', r':colon', line)
  355. line = re.sub(r'\?', r'?question-mark', line)
  356. line = re.sub(r'"', r'"quote', line)
  357. line = re.sub(r'([\w]) \' s', r"\1's", line)
  358. line = re.sub(r" '", r" 'single-quote", line)
  359. line = re.sub(r'\(', r'(left-paren', line)
  360. line = re.sub(r'\)', r')right-paren', line)
  361. line = re.sub(r'\[', r'[left-bracket', line)
  362. line = re.sub(r'\]', r']right-bracket', line)
  363. line = re.sub(r'{', r'{left-brace', line)
  364. line = re.sub(r'}', r'}right-brace', line)
  365. line = re.sub(r'!', r'!exclamation-point', line)
  366. line = re.sub(r';', r';semi-colon', line)
  367. line = re.sub(r'/', r'/slash', line)
  368. line = re.sub(r'%', r'%percent', line)
  369. line = re.sub(r'#', r'#sharp-sign', line)
  370. line = re.sub(r'@', r'@at-symbol', line)
  371. line = re.sub(r'\*', r'*asterisk', line)
  372. line = re.sub(r'\^', r'^circumflex', line)
  373. line = re.sub(r'&', r'&ampersand', line)
  374. line = re.sub(r'\$', r'$dollar-sign', line)
  375. line = re.sub(r'\+', r'+plus-symbol', line)
  376. line = re.sub(r'§', r'§section-sign', line)
  377. line = re.sub(r'¶', r'¶paragraph-sign', line)
  378. line = re.sub(r'¼', r'¼and-a-quarter', line)
  379. line = re.sub(r'½', r'½and-a-half', line)
  380. line = re.sub(r'¾', r'¾and-three-quarters', line)
  381. line = re.sub(r'¿', r'¿inverted-question-mark', line)
  382. line = re.sub(r'×', r'×multiplication-sign', line)
  383. line = re.sub(r'÷', r'÷division-sign', line)
  384. line = re.sub(r'° ', r'°degree-sign ', line)
  385. line = re.sub(r'©', r'©copyright-sign', line)
  386. line = re.sub(r'™', r'™trademark-sign', line)
  387. line = re.sub(r'®', r'®registered-sign', line)
  388. line = re.sub(r'_', r'_underscore', line)
  389. line = re.sub(r'\\', r'\backslash', line)
  390. line = re.sub(r'^(.)', r'<s> \1', line)
  391. line = re.sub(r'(.)$', r'\1 </s>', line)
  392. corpus[ind] = line
  393. return corpus
  394. def prepare_corpus(self, txt):
  395. txt.begin_user_action()
  396. self.bounds = self.textbuf.get_bounds()
  397. text = txt.get_text(self.bounds[0], self.bounds[1])
  398. # break on end of sentence
  399. text = re.sub(r'(\w[.:;?!])\s+(\w)', r'\1\n\2', text)
  400. text = re.sub(r'\n+', r'\n', text)
  401. corpus= re.split(r'\n', text)
  402. for ind, tex in enumerate(corpus):
  403. # try to remove blank lines
  404. tex = tex.strip()
  405. if len(tex) == 0:
  406. try:
  407. corpus.remove(ind)
  408. except:
  409. pass
  410. continue;
  411. # lower case maybe
  412. if len(tex) > 1 and tex[1] > 'Z':
  413. tex = tex[0].lower() + tex[1:]
  414. # separate punctuation marks into 'words'
  415. # by adding spaces between them
  416. tex = re.sub(r'\s*([^\w\s]|[_])\s*', r' \1 ', tex)
  417. # except apostrophe followed by lower-case letter
  418. tex = re.sub(r"(\w) ' ([a-z])", r"\1'\2", tex)
  419. tex = re.sub(r'\s+', ' ', tex)
  420. # fixme: needs more unicode -> dictionary replacements
  421. # or we could convert the rest of the dictionary to utf-8
  422. # and use the ʼunicode charactersʼ
  423. tex = tex.replace(u"ʼ", "'apostrophe")
  424. tex = tex.strip()
  425. corpus[ind] = tex
  426. return self.expand_punctuation(corpus)
  427. def asr_partial_result(self, asr, text, uttid):
  428. """Forward partial result signals on the bus to the main thread."""
  429. struct = gst.Structure('partial_result')
  430. struct.set_value('hyp', text)
  431. struct.set_value('uttid', uttid)
  432. asr.post_message(gst.message_new_application(asr, struct))
  433. def asr_result(self, asr, text, uttid):
  434. """Forward result signals on the bus to the main thread."""
  435. struct = gst.Structure('result')
  436. struct.set_value('hyp', text)
  437. struct.set_value('uttid', uttid)
  438. asr.post_message(gst.message_new_application(asr, struct))
  439. def application_message(self, bus, msg):
  440. """Receive application messages from the bus."""
  441. msgtype = msg.structure.get_name()
  442. if msgtype == 'partial_result':
  443. self.partial_result(msg.structure['hyp'],
  444. msg.structure['uttid'])
  445. elif msgtype == 'result':
  446. self.final_result(msg.structure['hyp'],
  447. msg.structure['uttid'])
  448. #self.pipeline.set_state(gst.STATE_PAUSED)
  449. #self.button.set_active(False)
  450. def partial_result(self, hyp, uttid):
  451. """Show partial result on tooltip."""
  452. self.text.set_tooltip_text(hyp)
  453. def final_result(self, hyp, uttid):
  454. """Insert the final result into the textbox."""
  455. # All this stuff appears as one single action
  456. self.textbuf.begin_user_action()
  457. self.text.set_tooltip_text(hyp)
  458. # get bounds of text buffer
  459. self.bounds = self.textbuf.get_bounds()
  460. # Fix punctuation
  461. hyp = self.collapse_punctuation(hyp, \
  462. not self.bounds[1].is_start())
  463. # handle commands
  464. if not self.do_command(hyp):
  465. self.undo.append(hyp)
  466. self.textbuf.delete_selection(True, self.text.get_editable())
  467. self.textbuf.insert_at_cursor(hyp)
  468. # send keystrokes to the desktop?
  469. if self.button1.get_active():
  470. send_string(hyp)
  471. display.sync()
  472. print(hyp)
  473. ins = self.textbuf.get_insert()
  474. iter = self.textbuf.get_iter_at_mark(ins)
  475. self.text.scroll_to_iter(iter, 0, False)
  476. self.textbuf.end_user_action()
  477. """Process spoken commands"""
  478. def err(self, errormsg):
  479. self.errmsg.label.set_text(errormsg)
  480. self.errmsg.run()
  481. self.errmsg.hide()
  482. def show_commands(self, argument=None):
  483. """ show these command preferences """
  484. me=self.prefsdialog
  485. self.commands_old = self.commands
  486. me.show_all()
  487. me.present()
  488. return True # command completed successfully!
  489. def clear_edits(self):
  490. """ close file and start over without saving """
  491. self.textbuf.set_text('')
  492. self.open_filename=''
  493. self.window.set_title("FreeSpeech")
  494. self.undo = []
  495. return True # command completed successfully!
  496. def backspace(self):
  497. """ delete one character """
  498. start = self.textbuf.get_iter_at_mark(self.textbuf.get_insert())
  499. self.textbuf.backspace(start, False, True)
  500. return True # command completed successfully!
  501. def select(self,argument=None):
  502. """ select [text/all/to end] """
  503. if argument:
  504. if re.match("^to end", argument):
  505. start = self.textbuf.get_iter_at_mark(self.textbuf.get_insert())
  506. end = self.bounds[1]
  507. self.textbuf.select_range(start, end)
  508. return True # success
  509. search_back = self.searchback(self.bounds[1], argument)
  510. if re.match("^all", argument):
  511. self.textbuf.select_range(self.bounds[0], self.bounds[1])
  512. return True # success
  513. search_back = self.searchback(self.bounds[1], argument)
  514. if None == search_back:
  515. return True
  516. # also select the space before it
  517. search_back[0].backward_char()
  518. self.textbuf.select_range(search_back[0], search_back[1])
  519. return True # command completed successfully!
  520. return False
  521. def delete(self,argument=None):
  522. """ delete [text] or erase selection """
  523. if argument:
  524. # print("del "+argument)
  525. if re.match("^to end", argument):
  526. start = self.textbuf.get_iter_at_mark(self.textbuf.get_insert())
  527. end = self.bounds[1]
  528. self.textbuf.delete(start, end)
  529. return True # success
  530. search_back = self.searchback(self.bounds[1], argument)
  531. if None == search_back:
  532. return True
  533. # also select the space before it
  534. search_back[0].backward_char()
  535. self.textbuf.delete(search_back[0], search_back[1])
  536. return True # command completed successfully!
  537. self.textbuf.delete_selection(True, self.text.get_editable())
  538. return True # command completed successfully!
  539. def insert(self,argument=None):
  540. """ insert after [text] """
  541. if re.match("^after", argument):
  542. argument = re.match(u'\w+(.*)', argument).group(1)
  543. search_back = self.searchback(self.bounds[1], argument)
  544. if None == search_back:
  545. return True
  546. self.textbuf.place_cursor(search_back[1])
  547. return True # command completed successfully!
  548. def done_editing(self):
  549. """ place cursor at end """
  550. self.textbuf.place_cursor(self.bounds[1])
  551. return True # command completed successfully!
  552. def scratch_that(self):
  553. """ erase recent text """
  554. if self.undo:
  555. scratch = self.undo.pop(-1)
  556. search_back = self.bounds[1].backward_search( \
  557. scratch, gtk.TEXT_SEARCH_TEXT_ONLY)
  558. if search_back:
  559. self.textbuf.select_range(search_back[0], search_back[1])
  560. self.textbuf.delete_selection(True, self.text.get_editable())
  561. if self.button1.get_active():
  562. b="".join(["\b" for x in range(0,len(scratch))])
  563. send_string(b)
  564. display.sync()
  565. return True # command completed successfully!
  566. return False
  567. def new_paragraph(self):
  568. """ start a new paragraph """
  569. self.textbuf.insert_at_cursor('\n')
  570. return True # command completed successfully!
  571. def file_open(self):
  572. """ open file dialog """
  573. response=self.file_chooser.run()
  574. if response==gtk.RESPONSE_ACCEPT:
  575. self.open_filename=self.file_chooser.get_filename()
  576. with codecs.open(self.open_filename, encoding='utf-8', mode='r') as f:
  577. self.textbuf.set_text(f.read())
  578. self.file_chooser.hide()
  579. self.window.set_title("FreeSpeech | "+ os.path.basename(self.open_filename))
  580. return True # command completed successfully!
  581. def file_save(self):
  582. """ save text buffer to disk """
  583. if not self.open_filename:
  584. response=self.file_chooser.run()
  585. if response==gtk.RESPONSE_ACCEPT:
  586. self.open_filename=self.file_chooser.get_filename()
  587. self.file_chooser.hide()
  588. self.window.set_title("FreeSpeech | "+ os.path.basename(self.open_filename))
  589. if self.open_filename:
  590. with codecs.open(self.open_filename, encoding='utf-8', mode='w') as f:
  591. f.write(self.textbuf.get_text(self.bounds[0],self.bounds[1]))
  592. return True # command completed successfully!
  593. def file_save_as(self):
  594. """ save under a different name """
  595. self.open_filename=''
  596. return self.file_save()
  597. def do_command(self, hyp):
  598. """decode spoken commands"""
  599. hyp = hyp.strip()
  600. hyp = hyp[0].lower() + hyp[1:]
  601. # editable commands
  602. commands=self.commands
  603. # process editable commands
  604. if commands.has_key(hyp):
  605. return eval(commands[hyp])()
  606. try:# separate command and arguments
  607. reg = re.match(u'(\w+) (.*)', hyp)
  608. command = reg.group(1)
  609. argument = reg.group(2)
  610. return eval(commands[command])(argument)
  611. except:
  612. pass # didn't work; not a command
  613. return False
  614. def searchback(self, iter, argument):
  615. """helper function to search backwards in text buffer"""
  616. search_back = iter.backward_search( \
  617. argument, gtk.TEXT_SEARCH_TEXT_ONLY)
  618. if None == search_back:
  619. search_back = iter.backward_search( \
  620. argument.capitalize(), gtk.TEXT_SEARCH_TEXT_ONLY)
  621. if None == search_back:
  622. return None
  623. return search_back
  624. if __name__ == "__main__":
  625. app = freespeech()
  626. gtk.main()