/jws.py

https://bitbucket.org/opensourcecoders/jws · Python · 463 lines · 423 code · 16 blank · 24 comment · 5 complexity · a183385eb7818d292f80a02d3f2c245a MD5 · raw file

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Just Wanna Say - Say what you types using google translate engine
  4. # Copyright (C) 2011 Thomaz de Oliveira dos Reis
  5. # Copyright (C) 2011 Dirley Rodrigues
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. import ctypes
  20. import hashlib
  21. import optparse
  22. import os
  23. import sys
  24. import tempfile
  25. import time
  26. import urllib
  27. import urllib2
  28. class Loader(object):
  29. def load(self, text, language):
  30. raise NotImplementedError
  31. class Storage(object):
  32. def store(self, identifier, fp):
  33. raise NotImplementedError
  34. def retrieve(self, identifier):
  35. """ Should raise ``Exception`` if not found.
  36. TODO: specialized exception """
  37. raise NotImplementedError
  38. def release(self, identifier):
  39. """ Release a identified file if it was stored here.
  40. TODO: specialized exception """
  41. raise NotImplementedError
  42. class Backend(object):
  43. named_file_required = None
  44. def __init__(self, *args):
  45. pass
  46. def play(self, fp):
  47. raise NotImplementedError
  48. @staticmethod
  49. def available():
  50. """ Subclasses must extend this method to tell the caller if they are
  51. available for use or not. """
  52. return True
  53. @classmethod
  54. def name(cls):
  55. return cls.__name__[:-len('_backend')]
  56. @classmethod
  57. def info(cls):
  58. return cls.__doc__.strip()
  59. @classmethod
  60. def availability_info(cls):
  61. doc = cls.available.__doc__
  62. if not doc is Backend.available.__doc__:
  63. return doc.strip()
  64. class DefaultLoader(Loader):
  65. def load(self, text, language):
  66. """ ``text`` must be unicode. ``language`` doesn't. """
  67. data = {
  68. 'tl': language,
  69. 'q': text.encode(sys.stdin.encoding or 'utf-8'),
  70. }
  71. url = 'http://translate.google.com/translate_tts?' + urllib.urlencode(data)
  72. request = urllib2.Request(url)
  73. request.add_header('User-Agent', 'Mozilla/5.0')
  74. webfile = urllib2.urlopen(request)
  75. return webfile
  76. class TempfileStorage(Storage):
  77. fmap = {}
  78. def store(self, identifier, fp):
  79. tf = open(tempfile.mktemp(suffix='.mp3'), 'wb')
  80. tf.write(fp.read())
  81. tf.close()
  82. self.fmap[identifier] = tf.name
  83. return self.retrieve(identifier)
  84. def retrieve(self, identifier):
  85. fp = None
  86. fname = self.fmap.get(identifier)
  87. if fname is not None:
  88. try:
  89. fp = open(fname, 'rb')
  90. except OSError:
  91. pass
  92. if fp is None:
  93. raise Exception('Not found')
  94. return fp
  95. def release(self, identifier):
  96. fname = self.fmap.get(identifier)
  97. if fname is not None:
  98. os.unlink(fname)
  99. class appkit_backend(Backend):
  100. """ An Apple's AppKit powered backend. """
  101. named_file_required = True
  102. def play(self, fp):
  103. from AppKit import NSSound
  104. sound = NSSound.alloc()
  105. sound.initWithContentsOfFile_byReference_(fp.name, True)
  106. sound.play()
  107. while sound.isPlaying():
  108. time.sleep(1)
  109. @classmethod
  110. def available(cls):
  111. """ Requires Apple AppKit available on MacOS X. """
  112. if not hasattr(cls,"_available"):
  113. try:
  114. import AppKit
  115. except ImportError:
  116. cls._available = False
  117. else:
  118. cls._available = True
  119. return cls._available
  120. class stdout_backend(Backend):
  121. """ A backend that prints the output to stdout. """
  122. def play(self, fp):
  123. print fp.read()
  124. class external_backend(Backend):
  125. """ A backend that uses a external program to play the audio. """
  126. named_file_required = True
  127. def __init__(self, command=""):
  128. if not command:
  129. command = self.autodetect_external_program()
  130. self.command = command
  131. @staticmethod
  132. def autodetect_external_program():
  133. external_programs = (
  134. ('mpg123', 'mplayer %s >/dev/null 2>&1'),
  135. ('playsound', 'playsound %s >/dev/null 2>&1'),
  136. ('mplayer', 'mplayer %s >/dev/null 2>&1'),
  137. )
  138. def is_exe(fpath):
  139. return os.path.exists(fpath) and os.access(fpath, os.X_OK)
  140. for program, command in external_programs:
  141. for path in os.environ['PATH'].split(os.pathsep):
  142. if is_exe(os.path.join(path, program)):
  143. return command
  144. def play(self, fp):
  145. command = self.command
  146. if not '%s' in self.command:
  147. command = self.command + ' %s'
  148. os.system(command % (fp.name,))
  149. class defaultapp_backend(external_backend):
  150. """ Try to use your default application as backend. """
  151. def __init__(self, *args):
  152. cmd = {'darwin': 'open %s',
  153. 'win32': 'cmd /c "start %s"',
  154. 'linux2': 'xdg-open %s'}.get(sys.platform)
  155. super(defaultapp_backend, self).__init__(cmd)
  156. class pyaudio_backend(Backend):
  157. """ A PortAudio and PyMAD powered backend. """
  158. def play(self, fp):
  159. import mad, pyaudio
  160. mf = mad.MadFile(fp)
  161. # open stream
  162. p = pyaudio.PyAudio()
  163. stream = p.open(format=p.get_format_from_width(pyaudio.paInt32),
  164. channels=2, rate=mf.samplerate(), output=True)
  165. data = mf.read()
  166. while data != None:
  167. stream.write(data)
  168. data = mf.read()
  169. stream.close()
  170. p.terminate()
  171. @classmethod
  172. def available(cls):
  173. """ Requires PyMad (http://spacepants.org/src/pymad/) and PyAudio (http://people.csail.mit.edu/hubert/pyaudio/). """
  174. if not hasattr(cls,"_available"):
  175. try:
  176. import mad, pyaudio
  177. except ImportError:
  178. cls._available = False
  179. else:
  180. cls._available = True
  181. return cls._available
  182. class ao_backend(Backend):
  183. """A LibAO and PyMAD powered backend. """
  184. def __init__(self, backend=None):
  185. self.backend = backend
  186. def play(self, fp):
  187. import mad, ao
  188. backend = self.backend
  189. if backend is None:
  190. import sys
  191. backend = {
  192. 'darwin': 'macosx',
  193. 'win32': 'wmm',
  194. 'linux2': 'alsa'
  195. }.get(sys.platform)
  196. if backend is None:
  197. raise Exception("Can't guess a usable libao baceknd."
  198. "If you know a backend that may work on your system then"
  199. "you can specify it using the backend options parameter.")
  200. mf = mad.MadFile(fp)
  201. dev = ao.AudioDevice(backend)
  202. while True:
  203. buf = mf.read()
  204. if buf is None:
  205. break
  206. dev.play(buf, len(buf))
  207. @classmethod
  208. def available(cls):
  209. """ Requires PyMad (http://spacepants.org/src/pymad/) and PyAO (http://ekyo.nerim.net/software/pyogg/). """
  210. if not hasattr(cls,"_available"):
  211. try:
  212. import mad,ao
  213. except ImportError:
  214. cls._available = False
  215. else:
  216. cls._available = True
  217. return cls._available
  218. class Win32AudioClip(object):
  219. """ A simple win32 audio clip. Inspired by mp3play (http://pypi.python.org/pypi/mp3play/) """
  220. def __init__(self, fname):
  221. self._alias = 'mp3_%s' % (id(self),)
  222. self._mci = ctypes.windll.winmm.mciSendStringA
  223. self._send(r'open "%s" alias %s' % (fname, self._alias))
  224. self._send('set %s time format milliseconds' % (self._alias,))
  225. length = self._send('status %s length' % (self._alias,))
  226. self._length = int(length)
  227. def _send(self, command):
  228. buf = ctypes.c_buffer(255)
  229. ret = self._mci(str(command), buf, 254, 0)
  230. if ret:
  231. err = int(ret)
  232. err_buf = ctypes.c_buffer(255)
  233. self._mci(err, err_buf, 254)
  234. raise RuntimeError("Error %d: %s" % (err, err_buf.vale))
  235. else:
  236. return buf.value
  237. def play(self):
  238. self._send('play %s from %d to %d' % (self._alias, 0, self._length))
  239. def isplaying(self):
  240. return 'playing' == self._send('status %s mode' % self._alias)
  241. def __del__(self):
  242. self._send('close %s' % (self._alias,))
  243. class win32_backend(Backend):
  244. """ The simplest backend available for Windows XP """
  245. named_file_required = True
  246. def play(self, fp):
  247. clip = Win32AudioClip(fp.name)
  248. clip.play()
  249. while clip.isplaying():
  250. time.sleep(1)
  251. @classmethod
  252. def available(cls):
  253. """ Runs on Windows XP or newer (?) """
  254. return sys.platform == 'win32'
  255. def autodetect_backend():
  256. # test for win32 backend
  257. if win32_backend.available():
  258. return win32_backend()
  259. # test for appkit
  260. if appkit_backend.available():
  261. return appkit_backend()
  262. # test for pyaudio
  263. if pyaudio_backend.available():
  264. return pyaudio_backend()
  265. # test for external programs
  266. cmd = external_backend.autodetect_external_program()
  267. if cmd is not None:
  268. return external_backend(cmd)
  269. # test for ao
  270. if ao_backend.available():
  271. return ao_backend()
  272. # using default app as backend
  273. print (u'No backend was found. Trying to play'
  274. u' using your default application')
  275. return defaultapp_backend()
  276. def installed_backends():
  277. """ Return installed backends, classified in available or unavailable. """
  278. # TODO: return in the order they are autodetected
  279. def recursive_backends(classes):
  280. result = classes
  281. for cls in classes:
  282. result += recursive_backends(cls.__subclasses__())
  283. return result
  284. backends = recursive_backends(Backend.__subclasses__())
  285. available = tuple(cls for cls in backends if cls.available())
  286. unavailable = tuple(cls for cls in backends if not cls.available())
  287. return available, unavailable
  288. def backends_help(extended=True):
  289. available, unavailable = installed_backends()
  290. print u'Available backends:'
  291. for backend in available:
  292. print '%s %s' % (backend.name().ljust(20), backend.info())
  293. if not extended: return
  294. print
  295. print u'Unavailable backends:'
  296. for backend in unavailable:
  297. print '%s %s \n %s' % (backend.name().ljust(20), backend.info(), " "*20+backend.availability_info())
  298. print
  299. print (u'To use the unavailable backends you must first supply their dependencies')
  300. def main():
  301. about= (u"Just Wanna Say [Version 2.1] Copyright (C) 2011 Thomaz Reis and Dirley Rodrigues"
  302. u"\nThis program comes with ABSOLUTELY NO WARRANTY;"
  303. u"\nThis is free software, and you are welcome to redistribute it under certain conditions;")
  304. usage = 'usage: %prog [options] [phrases]'
  305. option_list = [
  306. optparse.make_option('-h', '--help', action='store_true',
  307. dest='help', default=False, help=u'Show this help.'),
  308. optparse.make_option('--list-backends', action='store_true',
  309. dest='list_backends', default=False,
  310. help=u'List all installed backends.'),
  311. optparse.make_option('-l', '--language', action='store',
  312. type='string', dest='language', default='pt',
  313. help=u'Change the input language.'),
  314. optparse.make_option('-b', '--backend', action='store',
  315. type='string', dest='backend', default=None,
  316. help=u'Specify the audio output mean.'),
  317. optparse.make_option('-o', '--backend-options', action='store',
  318. type='string', dest='backend_options', default=None,
  319. help=u'Options to be passed to the backend.'),
  320. ]
  321. parser = optparse.OptionParser(usage=usage, option_list=option_list, add_help_option=False)
  322. options, arguments = parser.parse_args()
  323. if options.help:
  324. print about
  325. parser.print_help()
  326. print
  327. backends_help(options.list_backends)
  328. return
  329. elif options.list_backends:
  330. print about
  331. print
  332. backends_help(True)
  333. return
  334. elif not arguments:
  335. print about
  336. print
  337. print u'No arguments specified. Please, try -h or --help for usage information.'
  338. return
  339. if options.backend is not None:
  340. backend = globals().get('%s_backend' % (options.backend.lower(),))(options.backend_options)
  341. elif options.backend_options is not None:
  342. print u'You specified backend options but no backend.'
  343. return
  344. else:
  345. backend = autodetect_backend()
  346. text = u' '.join([i.decode(sys.stdin.encoding or 'utf-8') for i in arguments])
  347. #Just Wanna Have Fun :)
  348. if text == "Does JWS has any easter eggs?":
  349. if options.language == "pt":
  350. text = u"Infelizmente nĂŁo tem nenhum ister ĂŠgui nesse programa."
  351. else:
  352. text = u"Unfortunately there is no easter egg in this program."
  353. loader = DefaultLoader()
  354. lfp = loader.load(text, options.language)
  355. if backend.named_file_required:
  356. identifier = hashlib.md5(':'.join([options.language, text.encode('utf-8')])).hexdigest()
  357. storage = TempfileStorage()
  358. fp = storage.store(identifier, lfp)
  359. lfp.close()
  360. backend.play(fp)
  361. fp.close()
  362. else:
  363. backend.play(lfp)
  364. lfp.close()
  365. if __name__ == '__main__':
  366. main()