tts.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. # -*- coding: utf-8 -*-
  2. import base64
  3. import json
  4. import logging
  5. import re
  6. import urllib
  7. import requests
  8. from gtts.lang import _fallback_deprecated_lang, tts_langs
  9. from gtts.tokenizer import Tokenizer, pre_processors, tokenizer_cases
  10. from gtts.utils import _clean_tokens, _minimize, _translate_url
  11. __all__ = ["gTTS", "gTTSError"]
  12. # Logger
  13. log = logging.getLogger(__name__)
  14. log.addHandler(logging.NullHandler())
  15. class Speed:
  16. """Read Speed
  17. The Google TTS Translate API supports two speeds:
  18. Slow: True
  19. Normal: None
  20. """
  21. SLOW = True
  22. NORMAL = None
  23. class gTTS:
  24. """gTTS -- Google Text-to-Speech.
  25. An interface to Google Translate's Text-to-Speech API.
  26. Args:
  27. text (string): The text to be read.
  28. tld (string): Top-level domain for the Google Translate host,
  29. i.e `https://translate.google.<tld>`. Different Google domains
  30. can produce different localized 'accents' for a given
  31. language. This is also useful when ``google.com`` might be blocked
  32. within a network but a local or different Google host
  33. (e.g. ``google.com.hk``) is not. Default is ``com``.
  34. lang (string, optional): The language (IETF language tag) to
  35. read the text in. Default is ``en``.
  36. slow (bool, optional): Reads text more slowly. Defaults to ``False``.
  37. lang_check (bool, optional): Strictly enforce an existing ``lang``,
  38. to catch a language error early. If set to ``True``,
  39. a ``ValueError`` is raised if ``lang`` doesn't exist.
  40. Setting ``lang_check`` to ``False`` skips Web requests
  41. (to validate language) and therefore speeds up instantiation.
  42. Default is ``True``.
  43. pre_processor_funcs (list): A list of zero or more functions that are
  44. called to transform (pre-process) text before tokenizing. Those
  45. functions must take a string and return a string. Defaults to::
  46. [
  47. pre_processors.tone_marks,
  48. pre_processors.end_of_line,
  49. pre_processors.abbreviations,
  50. pre_processors.word_sub
  51. ]
  52. tokenizer_func (callable): A function that takes in a string and
  53. returns a list of string (tokens). Defaults to::
  54. Tokenizer([
  55. tokenizer_cases.tone_marks,
  56. tokenizer_cases.period_comma,
  57. tokenizer_cases.colon,
  58. tokenizer_cases.other_punctuation
  59. ]).run
  60. timeout (float or tuple, optional): Seconds to wait for the server to
  61. send data before giving up, as a float, or a ``(connect timeout,
  62. read timeout)`` tuple. ``None`` will wait forever (default).
  63. See Also:
  64. :doc:`Pre-processing and tokenizing <tokenizer>`
  65. Raises:
  66. AssertionError: When ``text`` is ``None`` or empty; when there's nothing
  67. left to speak after pre-precessing, tokenizing and cleaning.
  68. ValueError: When ``lang_check`` is ``True`` and ``lang`` is not supported.
  69. RuntimeError: When ``lang_check`` is ``True`` but there's an error loading
  70. the languages dictionary.
  71. """
  72. GOOGLE_TTS_MAX_CHARS = 100 # Max characters the Google TTS API takes at a time
  73. GOOGLE_TTS_HEADERS = {
  74. "Referer": "http://translate.google.com/",
  75. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) "
  76. "AppleWebKit/537.36 (KHTML, like Gecko) "
  77. "Chrome/47.0.2526.106 Safari/537.36",
  78. "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
  79. }
  80. GOOGLE_TTS_RPC = "jQ1olc"
  81. def __init__(
  82. self,
  83. text,
  84. tld="com",
  85. lang="en",
  86. slow=False,
  87. lang_check=True,
  88. pre_processor_funcs=[
  89. pre_processors.tone_marks,
  90. pre_processors.end_of_line,
  91. pre_processors.abbreviations,
  92. pre_processors.word_sub,
  93. ],
  94. tokenizer_func=Tokenizer(
  95. [
  96. tokenizer_cases.tone_marks,
  97. tokenizer_cases.period_comma,
  98. tokenizer_cases.colon,
  99. tokenizer_cases.other_punctuation,
  100. ]
  101. ).run,
  102. timeout=None,
  103. ):
  104. # Debug
  105. for k, v in dict(locals()).items():
  106. if k == "self":
  107. continue
  108. log.debug("%s: %s", k, v)
  109. # Text
  110. assert text, "No text to speak"
  111. self.text = text
  112. # Translate URL top-level domain
  113. self.tld = tld
  114. # Language
  115. self.lang_check = lang_check
  116. self.lang = lang
  117. if self.lang_check:
  118. # Fallback lang in case it is deprecated
  119. self.lang = _fallback_deprecated_lang(lang)
  120. try:
  121. langs = tts_langs()
  122. if self.lang not in langs:
  123. raise ValueError("Language not supported: %s" % lang)
  124. except RuntimeError as e:
  125. log.debug(str(e), exc_info=True)
  126. log.warning(str(e))
  127. # Read speed
  128. if slow:
  129. self.speed = Speed.SLOW
  130. else:
  131. self.speed = Speed.NORMAL
  132. # Pre-processors and tokenizer
  133. self.pre_processor_funcs = pre_processor_funcs
  134. self.tokenizer_func = tokenizer_func
  135. self.timeout = timeout
  136. def _tokenize(self, text):
  137. # Pre-clean
  138. text = text.strip()
  139. # Apply pre-processors
  140. for pp in self.pre_processor_funcs:
  141. log.debug("pre-processing: %s", pp)
  142. text = pp(text)
  143. if len(text) <= self.GOOGLE_TTS_MAX_CHARS:
  144. return _clean_tokens([text])
  145. # Tokenize
  146. log.debug("tokenizing: %s", self.tokenizer_func)
  147. tokens = self.tokenizer_func(text)
  148. # Clean
  149. tokens = _clean_tokens(tokens)
  150. # Minimize
  151. min_tokens = []
  152. for t in tokens:
  153. min_tokens += _minimize(t, " ", self.GOOGLE_TTS_MAX_CHARS)
  154. # Filter empty tokens, post-minimize
  155. tokens = [t for t in min_tokens if t]
  156. return tokens
  157. def _prepare_requests(self):
  158. """Created the TTS API the request(s) without sending them.
  159. Returns:
  160. list: ``requests.PreparedRequests_``. <https://2.python-requests.org/en/master/api/#requests.PreparedRequest>`_``.
  161. """
  162. # TTS API URL
  163. translate_url = _translate_url(
  164. tld=self.tld, path="_/TranslateWebserverUi/data/batchexecute"
  165. )
  166. text_parts = self._tokenize(self.text)
  167. log.debug("text_parts: %s", str(text_parts))
  168. log.debug("text_parts: %i", len(text_parts))
  169. assert text_parts, "No text to send to TTS API"
  170. prepared_requests = []
  171. for idx, part in enumerate(text_parts):
  172. data = self._package_rpc(part)
  173. log.debug("data-%i: %s", idx, data)
  174. # Request
  175. r = requests.Request(
  176. method="POST",
  177. url=translate_url,
  178. data=data,
  179. headers=self.GOOGLE_TTS_HEADERS,
  180. )
  181. # Prepare request
  182. prepared_requests.append(r.prepare())
  183. return prepared_requests
  184. def _package_rpc(self, text):
  185. parameter = [text, self.lang, self.speed, "null"]
  186. escaped_parameter = json.dumps(parameter, separators=(",", ":"))
  187. rpc = [[[self.GOOGLE_TTS_RPC, escaped_parameter, None, "generic"]]]
  188. espaced_rpc = json.dumps(rpc, separators=(",", ":"))
  189. return "f.req={}&".format(urllib.parse.quote(espaced_rpc))
  190. def get_bodies(self):
  191. """Get TTS API request bodies(s) that would be sent to the TTS API.
  192. Returns:
  193. list: A list of TTS API request bodies to make.
  194. """
  195. return [pr.body for pr in self._prepare_requests()]
  196. def stream(self):
  197. """Do the TTS API request(s) and stream bytes
  198. Raises:
  199. :class:`gTTSError`: When there's an error with the API request.
  200. """
  201. # When disabling ssl verify in requests (for proxies and firewalls),
  202. # urllib3 prints an insecure warning on stdout. We disable that.
  203. try:
  204. requests.packages.urllib3.disable_warnings(
  205. requests.packages.urllib3.exceptions.InsecureRequestWarning
  206. )
  207. except:
  208. pass
  209. prepared_requests = self._prepare_requests()
  210. for idx, pr in enumerate(prepared_requests):
  211. try:
  212. with requests.Session() as s:
  213. # Send request
  214. r = s.send(
  215. request=pr,
  216. verify=False,
  217. proxies=urllib.request.getproxies(),
  218. timeout=self.timeout,
  219. )
  220. log.debug("headers-%i: %s", idx, r.request.headers)
  221. log.debug("url-%i: %s", idx, r.request.url)
  222. log.debug("status-%i: %s", idx, r.status_code)
  223. r.raise_for_status()
  224. except requests.exceptions.HTTPError as e: # pragma: no cover
  225. # Request successful, bad response
  226. log.debug(str(e))
  227. raise gTTSError(tts=self, response=r)
  228. except requests.exceptions.RequestException as e: # pragma: no cover
  229. # Request failed
  230. log.debug(str(e))
  231. raise gTTSError(tts=self)
  232. # Write
  233. for line in r.iter_lines(chunk_size=1024):
  234. decoded_line = line.decode("utf-8")
  235. if "jQ1olc" in decoded_line:
  236. audio_search = re.search(r'jQ1olc","\[\\"(.*)\\"]', decoded_line)
  237. if audio_search:
  238. as_bytes = audio_search.group(1).encode("ascii")
  239. yield base64.b64decode(as_bytes)
  240. else:
  241. # Request successful, good response,
  242. # no audio stream in response
  243. raise gTTSError(tts=self, response=r)
  244. log.debug("part-%i created", idx)
  245. def write_to_fp(self, fp):
  246. """Do the TTS API request(s) and write bytes to a file-like object.
  247. Args:
  248. fp (file object): Any file-like object to write the ``mp3`` to.
  249. Raises:
  250. :class:`gTTSError`: When there's an error with the API request.
  251. TypeError: When ``fp`` is not a file-like object that takes bytes.
  252. """
  253. try:
  254. for idx, decoded in enumerate(self.stream()):
  255. fp.write(decoded)
  256. log.debug("part-%i written to %s", idx, fp)
  257. except (AttributeError, TypeError) as e:
  258. raise TypeError(
  259. "'fp' is not a file-like object or it does not take bytes: %s" % str(e)
  260. )
  261. def save(self, savefile):
  262. """Do the TTS API request and write result to file.
  263. Args:
  264. savefile (string): The path and file name to save the ``mp3`` to.
  265. Raises:
  266. :class:`gTTSError`: When there's an error with the API request.
  267. """
  268. with open(str(savefile), "wb") as f:
  269. self.write_to_fp(f)
  270. log.debug("Saved to %s", savefile)
  271. class gTTSError(Exception):
  272. """Exception that uses context to present a meaningful error message"""
  273. def __init__(self, msg=None, **kwargs):
  274. self.tts = kwargs.pop("tts", None)
  275. self.rsp = kwargs.pop("response", None)
  276. if msg:
  277. self.msg = msg
  278. elif self.tts is not None:
  279. self.msg = self.infer_msg(self.tts, self.rsp)
  280. else:
  281. self.msg = None
  282. super(gTTSError, self).__init__(self.msg)
  283. def infer_msg(self, tts, rsp=None):
  284. """Attempt to guess what went wrong by using known
  285. information (e.g. http response) and observed behaviour
  286. """
  287. cause = "Unknown"
  288. if rsp is None:
  289. premise = "Failed to connect"
  290. if tts.tld != "com":
  291. host = _translate_url(tld=tts.tld)
  292. cause = "Host '{}' is not reachable".format(host)
  293. else:
  294. # rsp should be <requests.Response>
  295. # http://docs.python-requests.org/en/master/api/
  296. status = rsp.status_code
  297. reason = rsp.reason
  298. premise = "{:d} ({}) from TTS API".format(status, reason)
  299. if status == 403:
  300. cause = "Bad token or upstream API changes"
  301. elif status == 404 and tts.tld != "com":
  302. cause = "Unsupported tld '{}'".format(tts.tld)
  303. elif status == 200 and not tts.lang_check:
  304. cause = (
  305. "No audio stream in response. Unsupported language '%s'"
  306. % self.tts.lang
  307. )
  308. elif status >= 500:
  309. cause = "Upstream API error. Try again later."
  310. return "{}. Probable cause: {}".format(premise, cause)