From 0e82b3359a7ee7842ead57049cc01ada2b5f6ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Adamczak?= Date: Tue, 7 Jun 2016 15:41:14 +0200 Subject: [PATCH] First commit --- .gitignore | 146 +++++++++++++++++++ .travis.yml | 13 ++ LICENSE | 21 +++ MANIFEST.in | 7 + README.md | 123 ++++++++++++++++ bin/ivona-speak | 16 ++ ivona_speak/__init__.py | 0 ivona_speak/command_line.py | 117 +++++++++++++++ ivona_speak/test/files/maja_dzien_dobry.mp3 | Bin 0 -> 5530 bytes ivona_speak/test/files/salli_hello_world.mp3 | Bin 0 -> 6471 bytes ivona_speak/test/test_command_line.py | 126 ++++++++++++++++ requirements.txt | 1 + requirements/common.txt | 4 + requirements/dev.txt | 8 + setup.py | 78 ++++++++++ tox.ini | 17 +++ 16 files changed, 677 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100755 bin/ivona-speak create mode 100644 ivona_speak/__init__.py create mode 100644 ivona_speak/command_line.py create mode 100644 ivona_speak/test/files/maja_dzien_dobry.mp3 create mode 100644 ivona_speak/test/files/salli_hello_world.mp3 create mode 100644 ivona_speak/test/test_command_line.py create mode 100644 requirements.txt create mode 100644 requirements/common.txt create mode 100644 requirements/dev.txt create mode 100644 setup.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50f8221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ +### pawelad ### +.idea/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9537a17 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python + +matrix: + include: + - os: linux + python: 3.4 + env: TOXENV=py34 + +install: + - pip install tox --use-mirrors + +script: + - tox diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d32b762 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Pythonity + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..34cf365 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include *.md +include *.txt +include LICENSE +include tox.ini +recursive-include ivona_speak *.mp3 +recursive-include ivona_speak *.py +recursive-include requirements *.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..364e7fa --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Ivona, Speak! +[![Build Status](https://img.shields.io/travis/Pythonity/ivona-speak.svg)][ivona speak github] +[![PyPI Version](https://img.shields.io/pypi/v/ivona_speak.svg)][ivona speak pypi] +[![Python Versions](https://img.shields.io/pypi/pyversions/ivona_speak.svg)][ivona speak pypi] +[![License](https://img.shields.io/github/license/Pythonity/ivona-speak.svg)][license] + +Python (3) script that lets you easily convert passed text to +synthesized audio files, with help of Amazon's [IVONA][ivona]. All you +need is a pair of [keys][ivona keys] and this script. Yes, that's +*literally* everything you need to never speak again. If that's your +thing of course. + +If you want to use IVONA Speech Cloud directly inside your (probably +awesome) Python project then have a look at +[python-ivona-api][ivona api github], which this script also uses. + +## Installation +With `pip` (recommended): +```shell +$ pip3 install ivona_speak +``` + +Without `pip`: +```shell +$ git clone https://github.com/Pythonity/ivona-speak +$ pip install -r ivona-speak/requirements.txt +$ cd icon-font-to-png/bin +``` + +## Usage +The script comes with two subcommands (`synthesize` is the default one): +```shell +$ ivona-speak synthesize -h +Usage: ivona-speak synthesize [OPTIONS] TEXT + + Synthesize passed text and save it as an audio file + +Options: + --access-key TEXT IVONA Speech Cloud access key. + --secret-key TEXT IVONA Speech Cloud secret key. + -a, --auth-file FILENAME Path to YAML file with 'access-key' and 'secret- + key' set. + -o, --output-file PATH Output audio file path. [required] + -n, --voice-name TEXT Voice name (default: Salli). + -l, --voice-language TEXT Voice language (default: en-US). + -c, --codec [ogg|mp3|mp4] Used codec (default: mp3). + -h, --help Show this message and exit. +``` + +```shell +$ ivona-speak list-voices -h +Usage: ivona-speak list-voices [OPTIONS] + + List available Ivona voices + +Options: + --access-key TEXT IVONA Speech Cloud access key. + --secret-key TEXT IVONA Speech Cloud secret key. + -a, --auth-file FILENAME Path to YAML file with 'access-key' and 'secret- + key' set. + -l, --voice-language TEXT Filter voice by language. + -h, --help Show this message and exit. +``` + +## Examples +With above usage everything should be pretty clear, but in case it +isn't: + +You can provide keys either explicitly or put them in YAML file (one of +those ways is required): +`$ ivona-speak list-voices --access-key 'YOUR_ACTUAL_ACCESS_KEY' --secret-key 'YOUR_ACTUAL_SECRET_KEY'` +`$ ivona-speak list-voices -a secrets.yaml` + +Also, `synthesized` is the default subcommand so those do the same: +`$ ivona-speak synthesize -a secrets.yaml -o hello_world.mp3 'Hello world!'` +`$ ivona-speak -a secrets.yaml -o hello_world.mp3 'Hello world!'` + +List all available IVONA voices, and list them now: +`$ ivona-speak list-voices -a secrets.yaml` + +I want someone to say 'Hello world!', and say it quick: +`$ ivona-speak synthesize -a secrets.yaml -o hello_world.mp3 'Hello world!'` + +She sounds so nice. I want someone special to respond her: +`$ ivona-speak synthesize -a secrets.yaml -o response.mp3 -n Joey 'How you doin?'` + +### Example auth file +```shell +$ cat secrets.yaml +access-key: YOUR_ACTUAL_ACCESS_KEY +secret-key: YOUR_ACTUAL_SECRET_KEY +``` + +## Tests +Package was tested with `pytest` and `tox` on Python 3.4 +(see `tox.ini`). + +To run tests yourself you need to set environment variables with secret +and access keys before running `tox` inside the repository: +```shell +export IVONA_ACCESS_KEY="YOUR_ACTUAL_ACCESS_KEY" +export IVONA_SECRET_KEY="YOUR_ACTUAL_SECRET_KEY" +``` + +## Contributions +Package source code is available at [GitHub][ivona speak github]. + +Feel free to use, ask, fork, star, report bugs, fix them, suggest +enhancements and point out any mistakes. + +## Authors +Developed and maintained by [Pythonity][pythonity]. + +Written by [Paweł Adamczak][pawelad]. + +[ivona speak github]: https://github.com/Pythonity/ivona-speak +[ivona speak pypi]: https://pypi.python.org/pypi/ivona_peak +[license]: https://github.com/Pythonity/ivona-speak/blob/master/LICENSE +[ivona]: https://www.ivona.com/ +[ivona keys]: http://developer.ivona.com/en/speechcloud/introduction.html#Credentials +[ivona api github]: https://github.com/Pythonity/python-ivona-api +[pythonity]: http://pythonity.com/ +[pawelad]: https://github.com/pawelad \ No newline at end of file diff --git a/bin/ivona-speak b/bin/ivona-speak new file mode 100755 index 0000000..0ed0553 --- /dev/null +++ b/bin/ivona-speak @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import os +import sys + +try: + # Installed system wide + from ivona_speak.command_line import cli +except ImportError: + # Locally + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + from ivona_speak.command_line import cli + + +if __name__ == '__main__': + cli() diff --git a/ivona_speak/__init__.py b/ivona_speak/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ivona_speak/command_line.py b/ivona_speak/command_line.py new file mode 100644 index 0000000..68fb3b3 --- /dev/null +++ b/ivona_speak/command_line.py @@ -0,0 +1,117 @@ +from itertools import groupby + +import yaml +import click +from click_default_group import DefaultGroup +from ivona_api.ivona_api import IvonaAPI, IvonaAPIException + + +@click.group(cls=DefaultGroup, default='synthesize', default_if_no_args=True, + context_settings=dict(help_option_names=['-h', '--help'])) +def cli(): + pass + + +@cli.command(name='synthesize') +@click.option('--access-key', type=str, + help="IVONA Speech Cloud access key.") +@click.option('--secret-key', type=str, + help="IVONA Speech Cloud secret key.") +@click.option('--auth-file', '-a', type=click.File(), + help="Path to YAML file with 'access-key' and 'secret-key' set.") +@click.option('--output-file', '-o', required=True, + type=click.Path(dir_okay=False, writable=True), + help="Output audio file path.") +@click.option('--voice-name', '-n', type=str, default='Salli', + help="Voice name (default: Salli).") +@click.option('--voice-language', '-l', type=str, default='en-US', + help="Voice language (default: en-US).") +@click.option('--codec', '-c', type=click.Choice(IvonaAPI.ALLOWED_CODECS), + default='mp3', help="Used codec (default: mp3).") +@click.argument('text', type=str) +def synthesize(access_key, secret_key, auth_file, output_file, voice_name, + voice_language, codec, text): + """Synthesize passed text and save it as an audio file""" + access_key, secret_key = _get_config_keys(access_key, secret_key, auth_file) + + try: + ivona_api = IvonaAPI( + access_key, secret_key, + voice_name=voice_name, language=voice_language, codec=codec, + ) + except IvonaAPIException: + raise click.ClickException("Given auth keys are incorrect.") + + with click.open_file(output_file, 'wb') as file: + ivona_api.text_to_speech(text, file) + + click.secho( + "File successfully saved as '{}'".format(output_file), fg='green' + ) + + +@cli.command(name='list-voices') +@click.option('--access-key', type=str, + help="IVONA Speech Cloud access key.") +@click.option('--secret-key', type=str, + help="IVONA Speech Cloud secret key.") +@click.option('--auth-file', '-a', type=click.File(), + help="Path to YAML file with 'access-key' and 'secret-key' set.") +@click.option('--voice-language', '-l', type=str, + help="Filter voice by language.") +def list_voices(access_key, secret_key, auth_file, voice_language): + """List available Ivona voices""" + access_key, secret_key = _get_config_keys(access_key, secret_key, auth_file) + + try: + ivona_api = IvonaAPI(access_key, secret_key) + except IvonaAPIException: + raise click.ClickException("Given auth keys are incorrect.") + + click.echo("Listing available voices...") + + # Possibly filter voices by language + if voice_language: + try: + voices_list = ivona_api.get_available_voices(voice_language) + except ValueError: + raise click.ClickException("Given filter language is incorrect.") + else: + voices_list = ivona_api.available_voices + + # Group voices by language + voices_dict = dict() + data = sorted(voices_list, key=lambda x: x['Language']) + for k, g in groupby(data, key=lambda x: x['Language']): + voices_dict[k] = list(g) + + for ln, voices in voices_dict.items(): + voice_names = [v['Name'] for v in voices] + click.echo("{}: {}".format(ln, ', '.join(voice_names))) + + click.secho("All done", fg='green') + + +def _get_config_keys(access_key, secret_key, yaml_file): + """ + Access and secret key must be either explicitly passed or be in YAML file + """ + if yaml_file: + config = yaml.safe_load(yaml_file) + + try: + access_key = config['access-key'] + secret_key = config['secret-key'] + except (TypeError, KeyError): + raise click.ClickException( + "Passed YAML file doesn't have needed " + "('access-key' and 'secret_key') values." + ) + + if not access_key or not secret_key: + raise click.ClickException("Both access key and secret key are needed.") + + return access_key, secret_key + +if __name__ == '__main__': + cli() diff --git a/ivona_speak/test/files/maja_dzien_dobry.mp3 b/ivona_speak/test/files/maja_dzien_dobry.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..20d5def15bb11fa50658b2943930cb71c99f10e8 GIT binary patch literal 5530 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 literal 0 HcmV?d00001 diff --git a/ivona_speak/test/files/salli_hello_world.mp3 b/ivona_speak/test/files/salli_hello_world.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..9f1cf9f05981cc226d512566b1b0faa397f67c43 GIT binary patch literal 6471 zcmc(j*H;s5)VBiy1_(%(UJ_~uz4v0MQbQ3CklrE+h+rjjAtJp9N(mhV6c7*)=`A$r zT{=qjk){I4yyksRzW?C64`$YyIl0z#|L#3|wh>Z}0z_IcYbz_gfA?!3&=nKc5HAHq zNqK2W8EKjSefxhG;Mk=H#8bcgAIPPTjE2J*vAjIasGQHJwbBK;ei(d$6rtx&)}>&G zdpK5hZuRy3Tao4M2u9kkYnaIs|x8m|~YM&Xl8FZeu z2w_4ayFkJWARUa;%I^oph!do4P2lp1@kNe>92geI0OIQg7wc;%ewIFAPEoo<w*`+z*>%P>< z*O!`^o<&f#6P(k?5@BqQpk*$?^OGjK3#c%`8%8q3)3J^%7nFw^G^?TCM@q22QhNka ziZz>3qEk#QWz=#EG3F?crJjL)Mf=bAUOeND`PMq4uDSLpkXqqa@W^^s1PBFYmKPOd zprp3^U_{GUG&`zBf|C{5FcW0L>$g!%`xTXoV9sF5sDo;7k^NhnyB>+D6Nd4{bS^E! zmpwWKpdJ>BC@>k4?3Kx5N6sj=&45Vg*KjZX!lfMZ5e}0S4r-BW#m{gR@6MIyHP)D~ zJX{M%=n%T)P8FhPB_&N}%YRX?tIBSZ&oJcL!uf=-g=9q=yL=UTbi=$wrF@~b7VJOw zUVUyg5+qQlt|du=gNg=)7@l;~A#c+g4?JI1P{xVPY;Xt$=pNiqP;&RV@;D2M)HFGG zQS(XBJU&2OJSsGk+ABa%x-GM>Ee^%ZZLH-;WzA+zJft_-RC|8%eFfM*LtO0$1lvmo z$1<~<9gLOFeDmTnyzRcy_FEaip!?l&Z=dl9esSWw)+x8s(-9)#6ay?zmx>HSqSwHA zcB7qG=8ZrQh7>sl0Vvi36bAW`#<<7-pQAB_oM6YQ+}CXbiT=RcGm3o;Dt>sf&h%Np zm2Oz>^R?&S?wXM?0=Ws;d#L{+3>o^yh{H#dT@1QQ#V`y8004TEy!CR|IMmg@!YIb} zXZDj-&-^uR%qXM@XJ`Kgp627-=!}!czgmI!=K*5QwG|*`%BYAFjIR@f2M zPbGfL-6o0S6gv8nEH5`V@KX1%6>==vlz;c$O=?Kop8s4(#gDx|byX5U(-_euS_#Ch zTTMKQJux&2iT9>0P9{7BHiw6Y2cM0o{K?`7fUwE0_Qsoq)fDoTS!d@t(Xio7@^!`X z8}y*MF#$U5Q?J~m)Lx3evQ3kdT@JGY#q{QmT^Nc&e-g~}SIi4j!yj{mw62`C_-eY&?OrqR~fYW@1`s(|k$A88EEGyeM<7840rZ=@K06$Uw{bWk3 z*caygp2fwELfwbiBb=QTB2%pn=Mp*x-n($~@FqE%Mq+Lg&&`XAc^?Z%wD)#MTfiD5 zluD2P56*_feJW4YeM!uzoor?x3Br9veH3>aI+0jC85uCC9}UFC3=Z&`rnN z`m-xR1h79J+f{xngUftsyz68*JM3ARk&j9(cOMexJr!yv>Th6#y9o!a`2P2~haP-kV+JFvw)iRa$s+Y=A==l@5WxB#vhQ!GR z;=w;of)b)szi?_()pA+VO9RQm?jY<@FNQ)!;NueZZ;i}S(Q(x(e}Hx&7Y0eeAKw#7&BX?65oaDq+CdNT9%#3?2Pbp1f>57FgO{hfW~cIoWd8!cI?nA+&Ro^l7dgpS~8} z(`5x+=@$Q4;|rRjcPyyqQtaV>jdW>z=Nh0)Ycm?kvv+{yysB+?miyYvH4e=GU)jqOb%jySu&(_ zsohk{hf(pq4Mb(oZ&YpZ99$nO&S@|Y>yth%STFaSTX&OaKODdI=MK6P6`k-IL$bf5fA zk{!7)gK;(P*Oab^`0F1$aZESx{K>V2pl~p@n9}Q;+Th+tJ4Yklm#|4qAD?bFE8mgw z77dE-peMeCerU{twgmT)u^es>nWPubFZZ!OOGAea^7>>#f!XWF7=UoSLBMdisSes1B^*3fzJAm^n9xVeYKjawvfjC>V!c+8v+ z$R>d)^9ga=ki6vUsk&^*KHEu;>CLRW+{I)IN^BMJl~!B{%CgsFP3Fu8Onl;_c3KdP zb@Dr6M%AZJsD<~NO!pjCN-I-PqQm?5{8!%X(6R~Az8EVHod*bkFRYkT)!7mM?Y5(* zTvR1{l`R^O(141DsvkFF&AJ`#-Sn zH`QiFSsPK*QLmAJ^JgaS)N~1~lpq<8p48$azZo}8SBdfRY7V3GjY&_Ofqg9Zk+ckF zk^pO3HNEtJy6U5CxhALWL-;@ylpK3?VbtMcyY2RG8zGW77v5dxM$IAoz|uDK;YM;t zat3~oI_Z-dx)K1=K2KnjqrLn+qouFyr8Z!qUd_mQrD5Xci$yc*9VT5mj*E=KNuH^O>jx2`-qg1z(#v_>!JoC~?b+;h}!a zA`~P&Jz;^koms%$S7(rugMDy!P|NfY9o|_ISCR^Kc-=u>LWQ$zI60xOEf#_&1_$Ao;NNd6>0Uxf(%pyS%3MZOrl9N&;#Na;ou|WG$V#i6H(C2^bcLAf&_uxh0>4|om~ITcDmN6=$F45= zmg&-qS+=w`Z0I|zZ*%B0c3V*H5`X%06k|z(d(Vq+j0u}SZWdnCaZ~weUDx71|8f@C zYim~zbWH4l-izqqFA|vA^Z>kn{wo?|zNU!yo^d|H`t?&t-~hk8b_9cV6ur7ABZfG< zNAZH`ISq(R8+wPhz6`u73@^#l$fmAp-=6fLJt=rxWGuRUd_i9l+^V9hFJUy62+rye zNv36+Kz)udT(3ho#elD~L**Af)i+;%L4xb%U9_EW?ljQ!=c)jYd|gL`(--z|bKD)+ zc|co%t)H zCn~D$0nNF^oY=CrzIm~-e(wyp@zVjM$j$QIbZB($qJ#`1j0O%e?-*D{#uvyepI(d@ zptfaN(yP-yx9^tJF@y-8oKMHNtv-nwqO3XRJ5ND@K`>j;-w<}6QZC)HG`ihD?PZHg z%_1r;4^J1mp(_vvnGbQo89^W{E9)Mbn_EpHjK_^pnt{foYe3s5`cAy(nUcVYXMpBe zB-62PvM9rCr^=DB50ROU{lu1uK$5s0P_*pym(CbtneVDJ@odnoBH%AeFDZHL($$8$$bB_{SlfD(bs(i5rVswp#?_LHLbX15@Ehe3^oCerH&@Iu zw92x`6-#B|Zdza4Hgt706(Lt+wLZ-|6zR*P=y#*E)s5>7{(V5w=Vpi9-*T*#wu%+4 zUK01Dt$SE+Q{Fi~fiiOkfvL$zaEC(f#wpGVw0cpIW#Ut}fR}UWDtWGe!E^{JG4^`6GA@J4 z!L58U4Ae9@RamfQc7@LPf6c*e-up!C#!ot38KKB`6~t@t;fu`t$7Al9AL z96f}LHLIrZ*38eZrhs%2n_GS~Cf>j=WG{t}1x^<^_aS7hDeB&1<>k#hi?$LeGl(3Q?kN-`1Buen`I@Byqb8$w zJEoIN%{f{wTifjxNa9{gci)SKY*5+~vT@_>z}s_pWoAtX|5`VtjvJ-4J1CA`gh2WC zWh7ImD_HsAJuBc`Xg8X#qETKgooOep4KE+)-9hIYo~uyYcl-D2VGAXf*9-MAxm20Y zR$^{J7v8=pru4yDh}oB!2`XOul(pMkEng}TGB$ps)Bra~o%`9Z^;`ggvm}EuXcsKV zQ#`d?I&PW2pGi<9!Fh^tIYiS;=vjLy>w3}bs>76?9`SH-0Uv$Faj;fjpNHswN1~bo z*k7;dyd}QenJ(G^W$h12>CRml*MVX=eo3aR6_f!41#*zMp`ZjhMF13VMqe$P7-cFM z$M%{|Da!}+jy;-#og0CmY0$HV!XWDd>GV0`in-~Jy}G?sVy`(1o!?demC{MR*5 za1DfkqtBnRodP(}wgAyScYC#np$S%JHrXn7kriJqZXu&du)M{XD6S!$IS&414C|Us z7XDr`bg$iSRy#ueE-n-IEqsOQ1_N7?0ecp#V{U9#-)gSnxD^Ub?F}Ltx2E@O-5*$F zk93U?1MsBCx$`-whB$rQ(S&Y(VSX*D+jX3HYpgF04gS4rw0EblnmTBV} zGP9g&SGCe&+U@Se?IFLtMVzy_w??*tdO=w)Gqi4)**GmH{@Z?JL5V;0wm||x(#euj zBWKrR@=jr>_(~n;1A<>wS7wltmP3lD6=q$SDN1v5`(FcZtrH5P6tnBq=XMW6Bqoj_ za${eU#69A>?N;jCMT_vu6FwlYl2hoW%BP};57%%Q^X-AJ;3e0LtDv07-rn?MRImHk zC#63|Q^F8>Ya zfa9iiWia*3Z)sMn)`>EF9NY7X& z6A2E>m$OjaZl_I76Kr?LezQ?fncgir&~9QcOaBeCK}U3NeFB8p2jcO-=tlXN&+vCulx)|=B>18Z3S z-*)MZ>RYOKp6o|tOK%I14R<;%m-TxJg3K&8tk6w$zAd!!-V((3XW7S%?+&&D?_WfN zla{qg*O4*I_XQS7U)=v^{dxLm%}o>oydm3(oIyeW1Mbvtw)1H~y!&VHn}G6*)D`gS97Z8X9t^HKXst9PJ21rSI|S zc43oA-)^Iw=LZ{bA+-&J6WubWL(uox7DlHNm6Vz{}^!Bhe z$N>vxGn)w-x^I=>)3FJ6;ufG~^o&Q~w8^iMt?H`R2HY)&u>_4MEuSwLc1!*Vt_|G=9h(S6iR!UUih24{gl2c(496Qv?EXK{4GI)N4 z{m)=#!9)QId3?Nsr&ynck^mlG8`}gKfwbV3a(a1~;BdpI4;+9?I>ngyVoM6PBJ9bM zIY4}8WmRRbJB&amug6ACqA%@?d1vs(|7qyPPebeD%WYR|iRDKHB@}|4+Qzx+k9MqM z^!fFGcpEBu6NFKiNnPMzh5S~mQ(J7chZBSQ(Jz>(jQs5|QX%2xGrx0A$aiFDDv54L4M19QDJ>MYhF&7TWEBlmc2 zzvIj-E>T_;h{@Sx4?@>t2n%T_pBX;aSk~G!n^#0Xk~kz!F%t#IP#;n4ZUZ;&=e(4# zxe1|QwSIlhGP~hd+RD2`29KXU0H}l))u(srkgM?IpUK>y zqyVe%=|)l%hXlPAmF39?tZfXu#UlSb(8R`tkl>s+>;EP|43QcbV#A$Ec;U9Fi%S>1 TgNcr*o}8qLiljp2|EKjo`Wk)V literal 0 HcmV?d00001 diff --git a/ivona_speak/test/test_command_line.py b/ivona_speak/test/test_command_line.py new file mode 100644 index 0000000..83db4ed --- /dev/null +++ b/ivona_speak/test/test_command_line.py @@ -0,0 +1,126 @@ +import os +import filecmp +import tempfile +from uuid import uuid4 + +import pytest +from flaky import flaky +from click.testing import CliRunner + +from ivona_speak.command_line import cli + + +# Module fixtures +@pytest.fixture(scope='module') +def auth_keys(): + """Get working auth keys from environment variables""" + access_key = os.environ["IVONA_ACCESS_KEY"] + secret_key = os.environ["IVONA_SECRET_KEY"] + assert access_key and secret_key + + return access_key, secret_key + + +# Tests +@pytest.mark.parametrize('subcommand,extra_args', [ + ('synthesize', ['-o', tempfile.NamedTemporaryFile().name, 'Hello world']), + ('list-voices', []), +]) +def test_auth_keys(subcommand, extra_args): + """Test passing auth keys""" + runner = CliRunner() + + # No keys provided + args = [subcommand] + extra_args + + result = runner.invoke(cli, args) + assert isinstance(result.exception, SystemExit) + assert result.exit_code == 1 + + # Wrong keys provided + args = ([subcommand] + + ['--access-key', str(uuid4()), '--secret-key', str(uuid4())] + + extra_args) + + result = runner.invoke(cli, args) + assert isinstance(result.exception, SystemExit) + assert result.exit_code == 1 + + # Incorrect auth file provided + with tempfile.NamedTemporaryFile() as temp_file: + args = ([subcommand] + + ['--auth-file', temp_file.name] + + extra_args) + + result = runner.invoke(cli, args) + assert isinstance(result.exception, SystemExit) + assert result.exit_code == 1 + + +@flaky +@pytest.mark.parametrize('voice_name,voice_language,content,org_file', [ + ('Salli', 'en-US', 'Hello world', 'files/salli_hello_world.mp3'), + ('Maja', 'pl-PL', 'Dzień dobry', 'files/maja_dzien_dobry.mp3'), +]) +def test_synthesize(auth_keys, voice_name, voice_language, content, org_file): + """Test 'synthesize' subcommand""" + runner = CliRunner() + + with tempfile.NamedTemporaryFile() as temp_file: + args = [ + 'synthesize', + '--access-key', auth_keys[0], + '--secret-key', auth_keys[1], + '--output-file', temp_file.name, + '--voice-name', voice_name, + '--voice-language', voice_language, + content, + ] + result = runner.invoke(cli, args) + assert result.exit_code == 0 + assert result.output + + assert filecmp.cmp(org_file, temp_file.name) + + +@flaky +def test_list_voices(auth_keys): + """Test 'list-voice' subcommand""" + runner = CliRunner() + + args = ['list-voices', '--access-key', auth_keys[0], '--secret-key', + auth_keys[1]] + + result = runner.invoke(cli, args) + assert result.exit_code == 0 + assert result.output + + +@flaky +def test_list_voices_with_filter(auth_keys): + """Test 'list-voice' subcommand with filter""" + runner = CliRunner() + + # Correct filter + args = [ + 'list-voices', + '--access-key', auth_keys[0], + '--secret-key', auth_keys[1], + '--voice-language', 'en-US', + ] + + result = runner.invoke(cli, args) + assert result.exit_code == 0 + assert result.output + + # Incorrect filter + args = [ + 'list-voices', + '--access-key', auth_keys[0], + '--secret-key', auth_keys[1], + '--voice-language', str(uuid4()), + ] + + result = runner.invoke(cli, args) + assert isinstance(result.exception, SystemExit) + assert result.exit_code == 1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c58f7cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +-r requirements/common.txt diff --git a/requirements/common.txt b/requirements/common.txt new file mode 100644 index 0000000..885ec6f --- /dev/null +++ b/requirements/common.txt @@ -0,0 +1,4 @@ +ivona_api>=0.1.1 +click>=6.6 +click-default-group>=1.2 +PyYAML>=3.11 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..34aa271 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,8 @@ +-r common.txt +pytest +flaky +tox +twine +pypandoc +check-manifest +coverage diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c0693b6 --- /dev/null +++ b/setup.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function +import sys +from setuptools import setup, find_packages +from setuptools.command.test import test as TestCommand + + +class Tox(TestCommand): + user_options = [('tox-args=', 'a', "Arguments to pass to tox")] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.tox_args = None + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import tox + import shlex + args = self.tox_args + if args: + args = shlex.split(self.tox_args) + errno = tox.cmdline(args=args) + sys.exit(errno) + +# Convert description from markdown to reStructuredText +try: + import pypandoc + description = pypandoc.convert('README.md', 'rst') +except (IOError, ImportError): + description = '' + + +setup( + name='ivona_speak', + url='https://github.com/Pythonity/ivona-speak', + download_url='https://github.com/Pythonity/ivona-speak/releases/latest', + bugtrack_url='https://github.com/Pythonity/ivona-speak/issues', + version='0.1.0', + license='MIT License', + author='Pythonity', + author_email='pythonity@pythonity.com', + maintainer='Paweł Adamczak', + maintainer_email='pawel.adamczak@sidnet.info', + description="Python script that lets you easily convert text to " + "synthesized audio files, with help of Amazon's IVONA.", + long_description=description, + packages=find_packages(), + include_package_data=True, + tests_require=['tox'], + cmdclass={'test': Tox}, + install_requires=[ + 'ivona_api>=0.1.1', + 'click>=6.6', + 'click-default-group>=1.2', + 'PyYAML>=3.11', + ], + extras_require={ + 'testing': ['pytest', 'flaky'], + }, + scripts=['bin/ivona-speak'], + keywords='amazon ivona text to speech synthesize', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Utilities', + ], +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..671bdda --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist=py34 + +[testenv] +commands=py.test +changedir={toxinidir}/ivona_speak/test +setenv=PYTHONWARNINGS=all +passenv=IVONA_* +deps= + pytest + flaky + -rrequirements.txt + +[pytest] +python_files=*.py +python_functions=test_ +norecursedirs=.tox .git .eggs