diff --git a/README.md b/README.md index 32c1956a..9dedd757 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# aeneas +# aeneas **aeneas** is a Python/C library and a set of tools to automagically synchronize audio and text (aka forced alignment). -* Version: 1.7.0 -* Date: 2016-12-07 +* Version: 1.7.1 +* Date: 2016-12-21 * Developed by: [ReadBeyond](http://www.readbeyond.it/) * Lead Developer: [Alberto Pettarin](http://www.albertopettarin.it/) * License: the GNU Affero General Public License Version 3 (AGPL v3) * Contact: [aeneas@readbeyond.it](mailto:aeneas@readbeyond.it) * Quick Links: [Home](http://www.readbeyond.it/aeneas/) - [GitHub](https://github.com/readbeyond/aeneas/) - [PyPI](https://pypi.python.org/pypi/aeneas/) - [Docs](http://www.readbeyond.it/aeneas/docs/) - [Tutorial](http://www.readbeyond.it/aeneas/docs/clitutorial.html) - [Benchmark](https://readbeyond.github.io/aeneas-benchmark/) - [Mailing List](https://groups.google.com/d/forum/aeneas-forced-alignment) - [Web App](http://aeneasweb.org) - + ## Goal **aeneas** automatically generates a **synchronization map** @@ -109,7 +109,7 @@ The generic OS-independent procedure is simple: `espeak`, `ffmpeg`, `ffprobe`, `pip`, and `python` 3. First install `numpy` with `pip` and then `aeneas` (this order is important): - + ```bash pip install numpy pip install aeneas @@ -185,7 +185,7 @@ The generic OS-independent procedure is simple: ```bash python -m aeneas.tools.execute_job job.zip output_directory ``` - + File `job.zip` should contain a `config.txt` or `config.xml` configuration file, providing **aeneas** with all the information needed to parse the input assets @@ -251,7 +251,7 @@ which explains how to use the built-in command line tools. * Extensive test suite including 1,200+ unit/integration/performance tests, that run and must pass before each release -## Limitations and Missing Features +## Limitations and Missing Features * Audio should match the text: large portions of spurious text or audio might produce a wrong sync map * Audio is assumed to be spoken: not suitable for song captioning, YMMV for CC applications @@ -304,7 +304,7 @@ No copy rights were harmed in the making of this project. ## Supporting and Contributing -### Sponsors +### Sponsors * **July 2015**: [Michele Gianella](https://plus.google.com/+michelegianella/about) generously supported the development of the boundary adjustment code (v1.0.4) diff --git a/README.rst b/README.rst index 2cdc021d..a7189432 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ aeneas **aeneas** is a Python/C library and a set of tools to automagically synchronize audio and text (aka forced alignment). -- Version: 1.7.0 -- Date: 2016-12-07 +- Version: 1.7.1 +- Date: 2016-12-21 - Developed by: `ReadBeyond `__ - Lead Developer: `Alberto Pettarin `__ - License: the GNU Affero General Public License Version 3 (AGPL v3) diff --git a/VERSION b/VERSION index bd8bf882..943f9cbc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.0 +1.7.1 diff --git a/aeneas/__init__.py b/aeneas/__init__.py index 3289315a..58f34b74 100644 --- a/aeneas/__init__.py +++ b/aeneas/__init__.py @@ -35,4 +35,4 @@ """ __license__ = "GNU AGPL v3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" diff --git a/aeneas/cdtw/cdtw_setup.py b/aeneas/cdtw/cdtw_setup.py index d8de9a55..e0a55253 100644 --- a/aeneas/cdtw/cdtw_setup.py +++ b/aeneas/cdtw/cdtw_setup.py @@ -49,7 +49,7 @@ setup( name="cdtw", - version="1.7.0", + version="1.7.1", description="Python C Extension for computing the DTW as fast as your bare metal allows.", ext_modules=[CMODULE], include_dirs=[misc_util.get_numpy_include_dirs()] diff --git a/aeneas/cew/cew_setup.py b/aeneas/cew/cew_setup.py index 7c72b12e..75f647cd 100644 --- a/aeneas/cew/cew_setup.py +++ b/aeneas/cew/cew_setup.py @@ -47,7 +47,7 @@ setup( name="cew", - version="1.7.0", + version="1.7.1", description="Python C Extension for synthesizing text with eSpeak.", ext_modules=[CMODULE] ) diff --git a/aeneas/cfw/cfw_setup.py b/aeneas/cfw/cfw_setup.py index 1633764b..d98c19a6 100644 --- a/aeneas/cfw/cfw_setup.py +++ b/aeneas/cfw/cfw_setup.py @@ -54,7 +54,7 @@ setup( name="cfw", - version="1.7.0", + version="1.7.1", description="Python C Extension for synthesizing text with Festival.", ext_modules=[CMODULE] ) diff --git a/aeneas/cmfcc/cmfcc_setup.py b/aeneas/cmfcc/cmfcc_setup.py index aaf41a93..e7944edd 100644 --- a/aeneas/cmfcc/cmfcc_setup.py +++ b/aeneas/cmfcc/cmfcc_setup.py @@ -50,7 +50,7 @@ setup( name="cmfcc", - version="1.7.0", + version="1.7.1", description="Python C Extension for computing the MFCCs as fast as your bare metal allows.", ext_modules=[CMODULE], include_dirs=[misc_util.get_numpy_include_dirs()] diff --git a/aeneas/cwave/cwave_setup.py b/aeneas/cwave/cwave_setup.py index 8899a5fa..7f4a4f59 100644 --- a/aeneas/cwave/cwave_setup.py +++ b/aeneas/cwave/cwave_setup.py @@ -49,7 +49,7 @@ setup( name="cwave", - version="1.7.0", + version="1.7.1", description="Python C Extension for for reading WAVE files.", ext_modules=[CMODULE], include_dirs=[misc_util.get_numpy_include_dirs()] diff --git a/aeneas/globalfunctions.py b/aeneas/globalfunctions.py index 7be7f891..5523ae75 100644 --- a/aeneas/globalfunctions.py +++ b/aeneas/globalfunctions.py @@ -1005,18 +1005,27 @@ def delete_directory(path): pass -def delete_file(handler, path): +def close_file_handler(handler): """ - Safely delete file. + Safely close the given file handler. :param object handler: the file handler (as returned by tempfile) - :param string path: the file path """ if handler is not None: try: os.close(handler) except: pass + + +def delete_file(handler, path): + """ + Safely delete file. + + :param object handler: the file handler (as returned by tempfile) + :param string path: the file path + """ + close_file_handler(handler) if path is not None: try: os.remove(path) diff --git a/aeneas/tests/test_globalfunctions.py b/aeneas/tests/test_globalfunctions.py index fa44ea5e..bfc06d89 100644 --- a/aeneas/tests/test_globalfunctions.py +++ b/aeneas/tests/test_globalfunctions.py @@ -562,6 +562,14 @@ def test_delete_directory_not_existing(self): gf.delete_directory(orig) self.assertFalse(gf.directory_exists(orig)) + def test_close_file_handler(self): + handler, path = gf.tmp_file() + self.assertTrue(gf.file_exists(path)) + gf.close_file_handler(handler) + self.assertTrue(gf.file_exists(path)) + gf.delete_file(handler, path) + self.assertFalse(gf.file_exists(path)) + def test_delete_file_existing(self): handler, path = gf.tmp_file() self.assertTrue(gf.file_exists(path)) diff --git a/aeneas/tools/abstract_cli_program.py b/aeneas/tools/abstract_cli_program.py index 2673a314..d5df3791 100644 --- a/aeneas/tools/abstract_cli_program.py +++ b/aeneas/tools/abstract_cli_program.py @@ -366,6 +366,7 @@ def run(self, arguments, show_help=True): # create logger self.logger = Logger(tee=self.verbose, tee_show_datetime=self.very_verbose) + self.log([u"Running aeneas %s", aeneas_version]) self.log([u"Formal arguments: %s", self.formal_arguments]) self.log([u"Actual arguments: %s", self.actual_arguments]) self.log([u"Runtime configuration: '%s'", self.rconf.config_string]) diff --git a/aeneas/tools/execute_task.py b/aeneas/tools/execute_task.py index 79a2cb3b..b3718d94 100644 --- a/aeneas/tools/execute_task.py +++ b/aeneas/tools/execute_task.py @@ -133,7 +133,7 @@ class ExecuteTaskCLI(AbstractCLIProgram): u"description": u"input: plain text, output: TSV, tts engine: Festival", u"audio": AUDIO_FILE, u"text": gf.relative_path("res/plain.txt", __file__), - u"config": u"task_language=eng|is_text_type=plain|os_task_file_format=tsv", + u"config": u"task_language=eng-USA|is_text_type=plain|os_task_file_format=tsv", u"syncmap": "output/sonnet.festival.tsv", u"options": u"-r=\"tts=festival\"", u"show": False @@ -205,7 +205,7 @@ class ExecuteTaskCLI(AbstractCLIProgram): u"description": u"input: multilevel plain text (mplain), different TTS engines, output: JSON", u"audio": AUDIO_FILE, u"text": gf.relative_path("res/mplain.txt", __file__), - u"config": u"task_language=eng|is_text_type=mplain|os_task_file_format=json", + u"config": u"task_language=eng-USA|is_text_type=mplain|os_task_file_format=json", u"syncmap": "output/sonnet.mplain.json", u"options": u"-r=\"tts_l1=festival|tts_l2=festival|tts_l3=espeak\"", u"show": False @@ -376,7 +376,7 @@ class ExecuteTaskCLI(AbstractCLIProgram): u"description": u"input: single word granularity plain text, output: AUD, tts engine: Festival, TTS cache on", u"audio": AUDIO_FILE, u"text": gf.relative_path("res/words.txt", __file__), - u"config": u"task_language=eng|is_text_type=plain|os_task_file_format=aud", + u"config": u"task_language=eng-USA|is_text_type=plain|os_task_file_format=aud", u"syncmap": "output/sonnet.words.aud", u"options": u"-r=\"tts=festival|tts_cache=True\"", u"show": False diff --git a/aeneas/ttswrappers/basettswrapper.py b/aeneas/ttswrappers/basettswrapper.py index aef15e82..e0d17ef7 100644 --- a/aeneas/ttswrappers/basettswrapper.py +++ b/aeneas/ttswrappers/basettswrapper.py @@ -470,7 +470,7 @@ def _synthesize_single_python_helper(self, text, voice_code, output_file_path=No return the audio data at the end of the function call; if ``False``, just return ``(True, None)`` in case of success. - :rtype: tuple (result, (duration, sample_rate, encoding, data)) or (result, None) + :rtype: tuple (result, (duration, sample_rate, codec, data)) or (result, None) """ raise NotImplementedError(u"This function must be implemented in concrete subclasses supporting Python call") @@ -489,7 +489,7 @@ def _synthesize_single_c_extension_helper(self, text, voice_code, output_file_pa If ``output_file_path`` is ``None``, the audio data will not persist to file at the end of the method. - :rtype: tuple (result, (duration, sample_rate, encoding, data)) + :rtype: tuple (result, (duration, sample_rate, codec, data)) """ raise NotImplementedError(u"This function might be implemented in concrete subclasses supporting C extension call") @@ -521,12 +521,12 @@ def _synthesize_single_subprocess_helper(self, text, voice_code, output_file_pat return the audio data at the end of the function call; if ``False``, just return ``(True, None)`` in case of success. - :rtype: tuple (result, (duration, sample_rate, encoding, data)) or (result, None) + :rtype: tuple (result, (duration, sample_rate, codec, data)) or (result, None) """ # return zero if text is the empty string if len(text) == 0: # - # NOTE sample_rate, encoding, data do not matter + # NOTE sample_rate, codec, data do not matter # if the duration is 0.000 => set them to None # self.log(u"len(text) is zero: returning 0.000") @@ -640,7 +640,7 @@ def _read_audio_data(self, file_path): """ Read audio data from file. - :rtype: tuple (True, (duration, sample_rate, encoding, data)) or (False, None) on exception + :rtype: tuple (True, (duration, sample_rate, codec, data)) or (False, None) on exception """ try: self.log(u"Reading audio data...") @@ -680,22 +680,30 @@ def _synthesize_multiple_generic(self, helper_function, text_file, output_file_p """ self.log(u"Calling TTS engine using multiple generic function...") - # get sample rate and encoding - self.log(u"Determining codec and sample rate with dummy text...") - succeeded, data = helper_function( - text=u"Dummy text to get sample_rate", - voice_code=self._language_to_voice_code(self.DEFAULT_LANGUAGE), - output_file_path=None - ) - if not succeeded: - self.log_crit(u"An unexpected error occurred in helper_function") - return (False, None) - du_nu, sample_rate, encoding, da_nu = data - self.log(u"Determining codec and sample rate with dummy text... done") + # get sample rate and codec + self.log(u"Determining codec and sample rate...") + if (self.OUTPUT_AUDIO_FORMAT is None) or (len(self.OUTPUT_AUDIO_FORMAT) != 3): + self.log(u"Determining codec and sample rate with dummy text...") + succeeded, data = helper_function( + text=u"Dummy text to get sample_rate", + voice_code=self._language_to_voice_code(self.DEFAULT_LANGUAGE), + output_file_path=None + ) + if not succeeded: + self.log_crit(u"An unexpected error occurred in helper_function") + return (False, None) + du_nu, sample_rate, codec, da_nu = data + self.log(u"Determining codec and sample rate with dummy text... done") + else: + self.log(u"Reading codec and sample rate from OUTPUT_AUDIO_FORMAT") + codec, channels_nu, sample_rate = self.OUTPUT_AUDIO_FORMAT + self.log(u"Determining codec and sample rate... done") + self.log([u" codec: %s", codec]) + self.log([u" sample rate: %d", sample_rate]) # open output file output_file = AudioFile(rconf=self.rconf, logger=self.logger) - output_file.audio_format = encoding + output_file.audio_format = codec output_file.audio_channels = 1 output_file.audio_sample_rate = sample_rate @@ -821,5 +829,7 @@ def _loop_use_cache(self, helper_function, num, fragment): self.log(u"Added fragment to cache") else: self.log(u"Fragment has zero duration, not adding it to cache") + self.log([u"Closing file handler for cached output file path '%s'", file_path]) + gf.close_file_handler(file_handler) self.log([u"Examining fragment %d (cache)... done", num]) return (True, data) diff --git a/aeneas/ttswrappers/festivalttswrapper.py b/aeneas/ttswrappers/festivalttswrapper.py index a7619967..073e051c 100644 --- a/aeneas/ttswrappers/festivalttswrapper.py +++ b/aeneas/ttswrappers/festivalttswrapper.py @@ -118,7 +118,7 @@ class FESTIVALTTSWrapper(BaseTTSWrapper): ENG_SCT: ENG_SCT, ENG_USA: ENG_USA, } - DEFAULT_LANGUAGE = ENG + DEFAULT_LANGUAGE = ENG_USA CODE_TO_HUMAN = { CES: u"Czech", diff --git a/aeneas_check_setup.py b/aeneas_check_setup.py index b635b3e6..ccdc4602 100644 --- a/aeneas_check_setup.py +++ b/aeneas_check_setup.py @@ -44,7 +44,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" ANSI_ERROR = u"\033[91m" ANSI_OK = u"\033[92m" diff --git a/bin/aeneas_check_setup.py b/bin/aeneas_check_setup.py index b635b3e6..ccdc4602 100755 --- a/bin/aeneas_check_setup.py +++ b/bin/aeneas_check_setup.py @@ -44,7 +44,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" ANSI_ERROR = u"\033[91m" ANSI_OK = u"\033[92m" diff --git a/bin/aeneas_convert_syncmap.py b/bin/aeneas_convert_syncmap.py index 028e1921..37b94055 100755 --- a/bin/aeneas_convert_syncmap.py +++ b/bin/aeneas_convert_syncmap.py @@ -40,7 +40,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_download.py b/bin/aeneas_download.py index 3d8fb2ae..dd03c5fd 100755 --- a/bin/aeneas_download.py +++ b/bin/aeneas_download.py @@ -40,7 +40,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_execute_job.py b/bin/aeneas_execute_job.py index 6d3bc444..9c081a37 100755 --- a/bin/aeneas_execute_job.py +++ b/bin/aeneas_execute_job.py @@ -42,7 +42,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_execute_task.py b/bin/aeneas_execute_task.py index eb4dd43e..0839bf17 100755 --- a/bin/aeneas_execute_task.py +++ b/bin/aeneas_execute_task.py @@ -41,7 +41,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_plot_waveform.py b/bin/aeneas_plot_waveform.py index 743b6646..81a32e35 100755 --- a/bin/aeneas_plot_waveform.py +++ b/bin/aeneas_plot_waveform.py @@ -40,7 +40,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_synthesize_text.py b/bin/aeneas_synthesize_text.py index 6db5aa68..8b49aa21 100755 --- a/bin/aeneas_synthesize_text.py +++ b/bin/aeneas_synthesize_text.py @@ -41,7 +41,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/bin/aeneas_validate.py b/bin/aeneas_validate.py index b0ebf7d9..8d74b703 100755 --- a/bin/aeneas_validate.py +++ b/bin/aeneas_validate.py @@ -47,7 +47,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/check_dependencies.py b/check_dependencies.py index b635b3e6..ccdc4602 100644 --- a/check_dependencies.py +++ b/check_dependencies.py @@ -44,7 +44,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" ANSI_ERROR = u"\033[91m" ANSI_OK = u"\033[92m" diff --git a/debian/changelog b/debian/changelog index 53e0d1f1..dce85a6e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +aeneas (1.7.1) stable; urgency=medium + + * Fix bug #151 + * Downgraded dependency on lxml to lxml>=3.6.0 to help packaging the Windows installer + * Added aeneas version to log + * Changed default voice for Festival TTS Wrapper to eng-USA to help people installing from source on Mac OS X + + -- alberto Sun, 18 Dec 2016 14:32:51 +0100 + aeneas (1.7.0) stable; urgency=medium * More robust and generic reading of SRT-like files, especially WebVTT diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index df580694..eb95b03b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,14 @@ Changelog ========= +v1.7.1 (2016-12-20) +------------------- + +#. Fix bug #151 +#. Downgraded dependency on lxml to lxml>=3.6.0 to help packaging the Windows installer +#. Added aeneas version to log +#. Changed default voice for Festival TTS Wrapper to ``eng-USA`` to help people installing from source on Mac OS X + v1.7.0 (2016-12-07) ------------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 4f2c136d..9b064f79 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,7 +60,7 @@ # The short X.Y version. version = '1.7' # The full version, including alpha/beta/rc tags. -release = '1.7.0' +release = '1.7.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/libtutorial.rst b/docs/source/libtutorial.rst index c4e75cef..a5cb346f 100644 --- a/docs/source/libtutorial.rst +++ b/docs/source/libtutorial.rst @@ -126,7 +126,7 @@ Dependencies ------------ * ``numpy`` (v1.9 or later) -* ``lxml`` (v3.6.4 or later) +* ``lxml`` (v3.6.0 or later) * ``BeautifulSoup`` (v4.5.1 or later) Only ``numpy`` is actually needed, as it is heavily used for the alignment computation. diff --git a/install_dependencies.sh b/install_dependencies.sh index e00e7bdc..047e82f4 100644 --- a/install_dependencies.sh +++ b/install_dependencies.sh @@ -7,7 +7,7 @@ # Copyright 2015-2016, Alberto Pettarin (www.albertopettarin.it) # """ # __license__ = "GNU AGPL 3" -# __version__ = "1.7.0" +# __version__ = "1.7.1" # __email__ = "aeneas@readbeyond.it" # __status__ = "Production" diff --git a/pyinstaller-aeneas-cli.py b/pyinstaller-aeneas-cli.py index 99165817..376205b7 100755 --- a/pyinstaller-aeneas-cli.py +++ b/pyinstaller-aeneas-cli.py @@ -42,7 +42,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" def main(): diff --git a/pyinstaller-onedir.spec b/pyinstaller-onedir.spec index 1094b49c..2185d3f7 100644 --- a/pyinstaller-onedir.spec +++ b/pyinstaller-onedir.spec @@ -9,7 +9,7 @@ #""" #__license__ = "GNU AGPL 3" #__status__ = "Production" -#__version__ = "1.7.0" +#__version__ = "1.7.1" datas = [ # required diff --git a/pyinstaller-onefile.spec b/pyinstaller-onefile.spec index f650ebe6..b0f7e0cc 100644 --- a/pyinstaller-onefile.spec +++ b/pyinstaller-onefile.spec @@ -9,7 +9,7 @@ #""" #__license__ = "GNU AGPL 3" #__status__ = "Production" -#__version__ = "1.7.0" +#__version__ = "1.7.1" datas = [ # required diff --git a/requirements.txt b/requirements.txt index bd84e7be..3edc5fd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ BeautifulSoup4>=4.5.1 -lxml>=3.6.4 +lxml>=3.6.0 numpy>=1.9 diff --git a/run_all_unit_tests.py b/run_all_unit_tests.py index fa1cf873..898ae02e 100644 --- a/run_all_unit_tests.py +++ b/run_all_unit_tests.py @@ -41,7 +41,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" TEST_DIRECTORY = "aeneas/tests" MAP = { diff --git a/setup.py b/setup.py index 55003e32..1020b860 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" ############################################################################## diff --git a/setupmeta.py b/setupmeta.py index ccc3074f..67790a21 100644 --- a/setupmeta.py +++ b/setupmeta.py @@ -36,7 +36,7 @@ """ __license__ = "GNU AGPL 3" __status__ = "Production" -__version__ = "1.7.0" +__version__ = "1.7.1" ############################################################################## @@ -47,14 +47,14 @@ # package version # NOTE: generate a new one for each PyPI upload, otherwise it will fail -PKG_VERSION = "1.7.0.0" +PKG_VERSION = "1.7.1.0" # required packages to install # NOTE: always use exact version numbers # NOTE: this list should be the same as requirements.txt PKG_INSTALL_REQUIRES = [ "BeautifulSoup4>=4.5.1", - "lxml>=3.6.4", + "lxml>=3.6.0", "numpy>=1.9" ]