Working single-file binary build

This commit is contained in:
Michael Hansen 2022-03-30 17:08:56 -04:00
commit b1d75d7ff6
16 changed files with 202 additions and 78 deletions

View file

@ -1,5 +1,7 @@
* *
!LICENSE !LICENSE
!install.sh
!pyinstaller/*.py
# opentts-abc # opentts-abc
!opentts-abc/setup.py !opentts-abc/setup.py
@ -14,6 +16,7 @@
!mimic3-tts/MANIFEST.in !mimic3-tts/MANIFEST.in
!mimic3-tts/requirements.txt !mimic3-tts/requirements.txt
!mimic3-tts/mimic3_tts/*.py !mimic3-tts/mimic3_tts/*.py
!mimic3-tts/mimic3_tts/voices.json
!mimic3-tts/mimic3_tts/VERSION !mimic3-tts/mimic3_tts/VERSION
# HTTP server # HTTP server

View file

@ -38,22 +38,13 @@ RUN --mount=type=cache,id=apt-build,target=/var/cache/apt \
python3 python3-pip python3-venv \ python3 python3-pip python3-venv \
build-essential python3-dev build-essential python3-dev
RUN mkdir -p /home/mimic3/app
WORKDIR /home/mimic3/app WORKDIR /home/mimic3/app
# Create virtual environment COPY ./ ./
RUN --mount=type=cache,id=pip-venv,target=/root/.cache/pip \
python3 -m venv .venv && \
.venv/bin/pip3 install --upgrade pip && \
.venv/bin/pip3 install --upgrade wheel setuptools
COPY ./ /home/mimic3/app/ # Install mimic3
# Install packages in order
RUN --mount=type=cache,id=pip-requirements,target=/root/.cache/pip \ RUN --mount=type=cache,id=pip-requirements,target=/root/.cache/pip \
.venv/bin/pip3 install -e opentts-abc && \ ./install.sh
.venv/bin/pip3 install -e mimic3-tts && \
.venv/bin/pip3 install -e mimic3-http
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

116
Dockerfile.binary Normal file
View file

@ -0,0 +1,116 @@
# Copyright 2022 Mycroft AI Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------------
# Dockerfile for Mimic 3 (https://github.com/MycroftAI/mimic3)
#
# Creates self-contained binaries using PyInstaller
# (https://pyinstaller.readthedocs.io/en/stable/).
#
# Requires Docker buildx: https://docs.docker.com/buildx/working-with-buildx/
# -----------------------------------------------------------------------------
FROM debian:bullseye as build
ARG TARGETARCH
ARG TARGETVARIANT
ENV LANG C.UTF-8
ENV DEBIAN_FRONTEND=noninteractive
RUN echo "Dir::Cache var/cache/apt/${TARGETARCH}${TARGETVARIANT};" > /etc/apt/apt.conf.d/01cache
RUN --mount=type=cache,id=apt-build,target=/var/cache/apt \
mkdir -p /var/cache/apt/${TARGETARCH}${TARGETVARIANT}/archives/partial && \
apt-get update && \
apt-get install --yes --no-install-recommends \
python3 python3-pip python3-venv \
build-essential python3-dev \
libespeak-ng1
WORKDIR /build/mimic3
COPY ./ ./
# Install mimic3
RUN --mount=type=cache,id=pip-venv,target=/root/.cache/pip \
./install.sh
# Install PyInstaller
RUN --mount=type=cache,id=pip-venv,target=/root/.cache/pip \
.venv/bin/pip3 install 'pyinstaller>=4,<5'
# Create binary for command-line interface
RUN .venv/bin/pyinstaller \
--onefile \
--noconfirm \
--name mimic3 \
--hidden-import 'pycrfsuite._dumpparser' \
--hidden-import 'pycrfsuite._logparser' \
--collect-binaries 'onnxruntime' \
--add-binary "$(/sbin/ldconfig -p | grep libespeak-ng.so.1 | awk '{print $4}'):." \
--collect-data 'gruut' \
--hidden-import "gruut_lang_de" \
--collect-data "gruut_lang_de" \
--hidden-import "gruut_lang_en" \
--collect-data "gruut_lang_en" \
--hidden-import "gruut_lang_fr" \
--collect-data "gruut_lang_fr" \
--hidden-import "gruut_lang_it" \
--collect-data "gruut_lang_it" \
--hidden-import "gruut_lang_nl" \
--collect-data "gruut_lang_nl" \
--hidden-import "gruut_lang_ru" \
--collect-data "gruut_lang_ru" \
--hidden-import "gruut_lang_sw" \
--collect-data "gruut_lang_sw" \
--collect-data 'espeak_phonemizer' \
--collect-data 'phonemes2ids' \
--collect-data 'mimic3_tts' \
pyinstaller/mimic3.py
# Create binary for web server
RUN .venv/bin/pyinstaller \
--onefile \
--noconfirm \
--name mimic3-server \
--hidden-import 'pycrfsuite._dumpparser' \
--hidden-import 'pycrfsuite._logparser' \
--collect-binaries 'onnxruntime' \
--add-binary "$(/sbin/ldconfig -p | grep libespeak-ng.so.1 | awk '{print $4}'):." \
--collect-data 'gruut' \
--hidden-import "gruut_lang_de" \
--collect-data "gruut_lang_de" \
--hidden-import "gruut_lang_en" \
--collect-data "gruut_lang_en" \
--hidden-import "gruut_lang_fr" \
--collect-data "gruut_lang_fr" \
--hidden-import "gruut_lang_it" \
--collect-data "gruut_lang_it" \
--hidden-import "gruut_lang_nl" \
--collect-data "gruut_lang_nl" \
--hidden-import "gruut_lang_ru" \
--collect-data "gruut_lang_ru" \
--hidden-import "gruut_lang_sw" \
--collect-data "gruut_lang_sw" \
--collect-data 'espeak_phonemizer' \
--collect-data 'phonemes2ids' \
--collect-data 'mimic3_tts' \
--collect-data 'mimic3_http' \
pyinstaller/mimic3_server.py
# -----------------------------------------------------------------------------
FROM scratch
COPY --from=build /build/mimic3/dist/ /

View file

@ -13,7 +13,10 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
.PHONY: dist install docker .PHONY: dist install docker binaries
SHELL := bash
DOCKER_PLATFORM := linux/amd64
dist: dist:
cd opentts-abc && python3 setup.py sdist cd opentts-abc && python3 setup.py sdist
@ -28,4 +31,7 @@ install:
./install.sh ./install.sh
docker: docker:
docker buildx build . -f Dockerfile --tag mycroftai/mimic3 --load docker buildx build . -f Dockerfile --platform $(DOCKER_PLATFORM) --tag mycroftai/mimic3 --load
binaries:
docker buildx build . -f Dockerfile.binary $(DOCKER_PLATFORM) --output type=local,dest=dist/$(DOCKER_PLATFORM)

View file

@ -6,10 +6,11 @@ this_dir="$( cd "$( dirname "$0" )" && pwd )"
# Kebab to snake case # Kebab to snake case
module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')" module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')"
base_dir="$(realpath "${this_dir}/..")"
src_dir="${this_dir}/${module_name}" src_dir="${this_dir}/${module_name}"
# Path to virtual environment # Path to virtual environment
: "${venv:=${this_dir}/.venv}" : "${venv:=${base_dir}/.venv}"
if [ -d "${venv}" ]; then if [ -d "${venv}" ]; then
# Activate virtual environment if available # Activate virtual environment if available

View file

@ -19,7 +19,10 @@ import csv
import io import io
import logging import logging
import os import os
import shlex
import shutil
import string import string
import subprocess
import sys import sys
import tempfile import tempfile
import threading import threading
@ -39,6 +42,7 @@ if typing.TYPE_CHECKING:
_LOGGER = logging.getLogger(_PACKAGE) _LOGGER = logging.getLogger(_PACKAGE)
_DEFAULT_PLAY_PROGRAMS = ["paplay", "play -q", "aplay -q"]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -264,7 +268,7 @@ def process_result(state: CommandLineInterfaceState):
wav_bytes = result.to_wav_bytes() wav_bytes = result.to_wav_bytes()
if wav_bytes: if wav_bytes:
play_wav_bytes(wav_bytes) play_wav_bytes(state.args, wav_bytes)
if args.output_dir: if args.output_dir:
if not wav_bytes: if not wav_bytes:
@ -387,7 +391,7 @@ def process_lines(state: CommandLineInterfaceState):
wav_file_play.setnchannels(state.num_channels) wav_file_play.setnchannels(state.num_channels)
wav_file_play.writeframes(state.all_audio) wav_file_play.writeframes(state.all_audio)
play_wav_bytes(wav_io.getvalue()) play_wav_bytes(state.args, wav_io.getvalue())
else: else:
# Write output directly to stdout # Write output directly to stdout
wav_file_write: wave.Wave_write = wave.open(sys.stdout.buffer, "wb") wav_file_write: wave.Wave_write = wave.open(sys.stdout.buffer, "wb")
@ -406,15 +410,20 @@ def shutdown_tts(state: CommandLineInterfaceState):
state.tts = None state.tts = None
def play_wav_bytes(wav_bytes: bytes): def play_wav_bytes(args: argparse.Namespace, wav_bytes: bytes):
from playsound import playsound
with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as wav_file: with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as wav_file:
wav_file.write(wav_bytes) wav_file.write(wav_bytes)
wav_file.seek(0) wav_file.seek(0)
_LOGGER.debug("Playing WAV file: %s", wav_file.name) for play_program in reversed(args.play_program):
playsound(wav_file.name) play_cmd = shlex.split(play_program)
if not shutil.which(play_cmd[0]):
continue
play_cmd.append(wav_file.name)
_LOGGER.debug("Playing WAV file: %s", play_cmd)
subprocess.check_output(play_cmd)
break
def print_voices(state: CommandLineInterfaceState): def print_voices(state: CommandLineInterfaceState):
@ -521,6 +530,12 @@ def get_args():
parser.add_argument( parser.add_argument(
"--preload-voice", action="append", help="Preload voice when starting up" "--preload-voice", action="append", help="Preload voice when starting up"
) )
parser.add_argument(
"--play-program",
action="append",
default=_DEFAULT_PLAY_PROGRAMS,
help="Program(s) used to play WAV files",
)
parser.add_argument("--seed", type=int, help="Set random seed (default: not set)") parser.add_argument("--seed", type=int, help="Set random seed (default: not set)")
parser.add_argument("--version", action="store_true", help="Print version and exit") parser.add_argument("--version", action="store_true", help="Print version and exit")
parser.add_argument( parser.add_argument(

View file

@ -19,5 +19,7 @@ from xdgenvpy import XDG
DEFAULT_VOICE = "en_US/vctk_low" DEFAULT_VOICE = "en_US/vctk_low"
DEFAULT_LANGUAGE = "en_US" DEFAULT_LANGUAGE = "en_US"
DEFAULT_VOICES_URL_FORMAT = "https://github.com/MycroftAI/mimic3-voices/raw/master/voices/{lang}/{name}" DEFAULT_VOICES_URL_FORMAT = (
"https://github.com/MycroftAI/mimic3-voices/raw/master/voices/{lang}/{name}"
)
DEFAULT_VOICES_DOWNLOAD_DIR = Path(XDG().XDG_DATA_HOME) / "mimic3" / "voices" DEFAULT_VOICES_DOWNLOAD_DIR = Path(XDG().XDG_DATA_HOME) / "mimic3" / "voices"

View file

@ -6,9 +6,6 @@ ignore_missing_imports = True
[mypy-onnxruntime.*] [mypy-onnxruntime.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-playsound.*]
ignore_missing_imports = True
[mypy-tqdm.*] [mypy-tqdm.*]
ignore_missing_imports = True ignore_missing_imports = True

View file

@ -1,10 +1,9 @@
dataclasses-json<1.0 dataclasses-json<1.0
espeak-phonemizer>=1.0,<2.0 espeak-phonemizer>=1.0,<2.0
gruut>=2.2.2,<3.0 gruut>=2.3.0,<3.0
numpy<2.0 numpy<2.0
onnxruntime>=1.6,<2.0 onnxruntime>=1.6,<2.0
opentts_abc<1.0 opentts_abc<1.0
phonemes2ids<2.0 phonemes2ids<2.0
playsound~=1.3.0
tqdm>=4,<5 tqdm>=4,<5
xdgenvpy>2.0,<3 xdgenvpy>2.0,<3

View file

@ -1,7 +0,0 @@
black==22.1.0
coverage==5.0.4
flake8==3.7.9
mypy==0.910
pylint==2.10.2
pytest==5.4.1
pytest-cov==2.8.1

View file

@ -81,7 +81,7 @@ setup(
author_email="michael.hansen@mycroft.ai", author_email="michael.hansen@mycroft.ai",
license="AGPLv3+", license="AGPLv3+",
packages=setuptools.find_packages(), packages=setuptools.find_packages(),
package_data={"mimic3_tts": ["VERSION", "py.typed"]}, package_data={"mimic3_tts": ["VERSION", "py.typed", "voices.json"]},
install_requires=requirements, install_requires=requirements,
extras_require={':python_version<"3.9"': ["importlib_resources"], **extras_require}, extras_require={':python_version<"3.9"': ["importlib_resources"], **extras_require},
entry_points={"console_scripts": ["mimic3 = mimic3_cli.__main__:main"]}, entry_points={"console_scripts": ["mimic3 = mimic3_cli.__main__:main"]},

View file

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
# Directory of *this* script
this_dir="$( cd "$( dirname "$0" )" && pwd )"
# Path to virtual environment
: "${venv:=${this_dir}/.venv}"
# Python binary to use
: "${PYTHON=python3}"
# pip install command
: "${PIP_INSTALL=install}"
python_version="$(${PYTHON} --version)"
# Create virtual environment
echo "Creating virtual environment at ${venv} (${python_version})"
rm -rf "${venv}"
"${PYTHON}" -m venv "${venv}"
source "${venv}/bin/activate"
# Install Python dependencies
echo 'Installing Python dependencies'
pip3 ${PIP_INSTALL} --upgrade pip
pip3 ${PIP_INSTALL} --upgrade wheel setuptools
find "${this_dir}" -name 'requirements*.txt' -type f -print0 | \
xargs -0 -n1 pip3 ${PIP_INSTALL} -r
# -----------------------------------------------------------------------------
echo "OK"

View file

@ -1,7 +0,0 @@
black==22.1.0
coverage==5.0.4
flake8==3.7.9
mypy==0.910
pylint==2.10.2
pytest==5.4.1
pytest-cov==2.8.1

21
pyinstaller/mimic3.py Normal file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# Copyright 2022 Mycroft AI Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""Stub for PyInstaller"""
from mimic3_tts.__main__ import main
main()

View file

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# Copyright 2022 Mycroft AI Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""Stub for PyInstaller"""
from mimic3_http.__main__ import main
main()

View file

@ -1,4 +1,4 @@
black==22.1.0 black==22.3.0
coverage==5.0.4 coverage==5.0.4
flake8==3.7.9 flake8==3.7.9
mypy==0.910 mypy==0.910