190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
from gtts import gTTS, gTTSError, __version__
|
|
from gtts.lang import tts_langs, _fallback_deprecated_lang
|
|
import click
|
|
import logging
|
|
import logging.config
|
|
|
|
# Click settings
|
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
|
|
# Logger settings
|
|
LOGGER_SETTINGS = {
|
|
"version": 1,
|
|
"formatters": {"default": {"format": "%(name)s - %(levelname)s - %(message)s"}},
|
|
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "default"}},
|
|
"loggers": {"gtts": {"handlers": ["console"], "level": "WARNING"}},
|
|
}
|
|
|
|
# Logger
|
|
logging.config.dictConfig(LOGGER_SETTINGS)
|
|
log = logging.getLogger("gtts")
|
|
|
|
|
|
def sys_encoding():
|
|
"""Charset to use for --file <path>|- (stdin)"""
|
|
return "utf8"
|
|
|
|
|
|
def validate_text(ctx, param, text):
|
|
"""Validation callback for the <text> argument.
|
|
Ensures <text> (arg) and <file> (opt) are mutually exclusive
|
|
"""
|
|
if not text and "file" not in ctx.params:
|
|
# No <text> and no <file>
|
|
raise click.BadParameter("<text> or -f/--file <file> required")
|
|
if text and "file" in ctx.params:
|
|
# Both <text> and <file>
|
|
raise click.BadParameter("<text> and -f/--file <file> can't be used together")
|
|
return text
|
|
|
|
|
|
def validate_lang(ctx, param, lang):
|
|
"""Validation callback for the <lang> option.
|
|
Ensures <lang> is a supported language unless the <nocheck> flag is set
|
|
"""
|
|
if ctx.params["nocheck"]:
|
|
return lang
|
|
|
|
# Fallback from deprecated language if needed
|
|
lang = _fallback_deprecated_lang(lang)
|
|
|
|
try:
|
|
if lang not in tts_langs():
|
|
raise click.UsageError(
|
|
"'%s' not in list of supported languages.\n"
|
|
"Use --all to list languages or "
|
|
"add --nocheck to disable language check." % lang
|
|
)
|
|
else:
|
|
# The language is valid.
|
|
# No need to let gTTS re-validate.
|
|
ctx.params["nocheck"] = True
|
|
except RuntimeError as e:
|
|
# Only case where the <nocheck> flag can be False
|
|
# Non-fatal. gTTS will try to re-validate.
|
|
log.debug(str(e), exc_info=True)
|
|
|
|
return lang
|
|
|
|
|
|
def print_languages(ctx, param, value):
|
|
"""Callback for <all> flag.
|
|
Prints formatted sorted list of supported languages and exits
|
|
"""
|
|
if not value or ctx.resilient_parsing:
|
|
return
|
|
|
|
try:
|
|
langs = tts_langs()
|
|
langs_str_list = sorted("{}: {}".format(k, langs[k]) for k in langs)
|
|
click.echo(" " + "\n ".join(langs_str_list))
|
|
except RuntimeError as e: # pragma: no cover
|
|
log.debug(str(e), exc_info=True)
|
|
raise click.ClickException("Couldn't fetch language list.")
|
|
ctx.exit()
|
|
|
|
|
|
def set_debug(ctx, param, debug):
|
|
"""Callback for <debug> flag.
|
|
Sets logger level to DEBUG
|
|
"""
|
|
if debug:
|
|
log.setLevel(logging.DEBUG)
|
|
return
|
|
|
|
|
|
@click.command(context_settings=CONTEXT_SETTINGS)
|
|
@click.argument("text", metavar="<text>", required=False, callback=validate_text)
|
|
@click.option(
|
|
"-f",
|
|
"--file",
|
|
metavar="<file>",
|
|
# For py2.7/unicode. If encoding not None Click uses io.open
|
|
type=click.File(encoding=sys_encoding()),
|
|
help="Read from <file> instead of <text>.",
|
|
)
|
|
@click.option(
|
|
"-o",
|
|
"--output",
|
|
metavar="<file>",
|
|
type=click.File(mode="wb"),
|
|
help="Write to <file> instead of stdout.",
|
|
)
|
|
@click.option("-s", "--slow", default=False, is_flag=True, help="Read more slowly.")
|
|
@click.option(
|
|
"-l",
|
|
"--lang",
|
|
metavar="<lang>",
|
|
default="en",
|
|
show_default=True,
|
|
callback=validate_lang,
|
|
help="IETF language tag. Language to speak in. List documented tags with --all.",
|
|
)
|
|
@click.option(
|
|
"-t",
|
|
"--tld",
|
|
metavar="<tld>",
|
|
default="com",
|
|
show_default=True,
|
|
is_eager=True, # Prioritize <tld> to ensure it gets set before <lang>
|
|
help="Top-level domain for the Google host, i.e https://translate.google.<tld>",
|
|
)
|
|
@click.option(
|
|
"--nocheck",
|
|
default=False,
|
|
is_flag=True,
|
|
is_eager=True, # Prioritize <nocheck> to ensure it gets set before <lang>
|
|
help="Disable strict IETF language tag checking. Allow undocumented tags.",
|
|
)
|
|
@click.option(
|
|
"--all",
|
|
default=False,
|
|
is_flag=True,
|
|
is_eager=True,
|
|
expose_value=False,
|
|
callback=print_languages,
|
|
help="Print all documented available IETF language tags and exit.",
|
|
)
|
|
@click.option(
|
|
"--debug",
|
|
default=False,
|
|
is_flag=True,
|
|
is_eager=True, # Prioritize <debug> to see debug logs of callbacks
|
|
expose_value=False,
|
|
callback=set_debug,
|
|
help="Show debug information.",
|
|
)
|
|
@click.version_option(version=__version__)
|
|
def tts_cli(text, file, output, slow, tld, lang, nocheck):
|
|
"""Read <text> to mp3 format using Google Translate's Text-to-Speech API
|
|
(set <text> or --file <file> to - for standard input)
|
|
"""
|
|
|
|
# stdin for <text>
|
|
if text == "-":
|
|
text = click.get_text_stream("stdin").read()
|
|
|
|
# stdout (when no <output>)
|
|
if not output:
|
|
output = click.get_binary_stream("stdout")
|
|
|
|
# <file> input (stdin on '-' is handled by click.File)
|
|
if file:
|
|
try:
|
|
text = file.read()
|
|
except UnicodeDecodeError as e: # pragma: no cover
|
|
log.debug(str(e), exc_info=True)
|
|
raise click.FileError(
|
|
file.name, "<file> must be encoded using '%s'." % sys_encoding()
|
|
)
|
|
|
|
# TTS
|
|
try:
|
|
tts = gTTS(text=text, lang=lang, slow=slow, tld=tld, lang_check=not nocheck)
|
|
tts.write_to_fp(output)
|
|
except (ValueError, AssertionError) as e:
|
|
raise click.UsageError(str(e))
|
|
except gTTSError as e:
|
|
raise click.ClickException(str(e))
|