Initial commit
This commit is contained in:
commit
1eae034d8c
72 changed files with 4689 additions and 0 deletions
0
.projectile
Normal file
0
.projectile
Normal file
201
LICENSE
Normal file
201
LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
14
mimic3-cli/.gitignore
vendored
Normal file
14
mimic3-cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
6
mimic3-cli/.isort.cfg
Normal file
6
mimic3-cli/.isort.cfg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
3
mimic3-cli/.projectile
Normal file
3
mimic3-cli/.projectile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- /.venv/
|
||||
- /.mypy_cache/
|
||||
- /mimic3_cli/.mypy_cache/
|
||||
201
mimic3-cli/LICENSE
Normal file
201
mimic3-cli/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
mimic3-cli/MANIFEST.in
Normal file
5
mimic3-cli/MANIFEST.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
include requirements.txt
|
||||
include requirements_dev.txt
|
||||
include LICENSE
|
||||
include README.md
|
||||
include mimic3_tts/VERSION
|
||||
28
mimic3-cli/check.sh
Executable file
28
mimic3-cli/check.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# Directory of *this* script
|
||||
this_dir="$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
# Kebab to snake case
|
||||
module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')"
|
||||
src_dir="${this_dir}/${module_name}"
|
||||
|
||||
# Path to virtual environment
|
||||
: "${venv:=${this_dir}/.venv}"
|
||||
|
||||
if [ -d "${venv}" ]; then
|
||||
# Activate virtual environment if available
|
||||
source "${venv}/bin/activate"
|
||||
fi
|
||||
|
||||
# Format code
|
||||
black "${src_dir}"
|
||||
isort "${src_dir}"
|
||||
|
||||
# Check
|
||||
flake8 "${src_dir}"
|
||||
pylint "${src_dir}"
|
||||
mypy "${src_dir}"
|
||||
|
||||
echo 'OK'
|
||||
34
mimic3-cli/install.sh
Executable file
34
mimic3-cli/install.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/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"
|
||||
14
mimic3-cli/mimic3_cli/.gitignore
vendored
Normal file
14
mimic3-cli/mimic3_cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
1
mimic3-cli/mimic3_cli/__init__.py
Normal file
1
mimic3-cli/mimic3_cli/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python3
|
||||
694
mimic3-cli/mimic3_cli/__main__.py
Normal file
694
mimic3-cli/mimic3_cli/__main__.py
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import wave
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from mimic3_tts import Mimic3TextToSpeechSystem
|
||||
|
||||
|
||||
_DIR = Path(__file__).parent
|
||||
_PACKAGE = "mimic3_cli"
|
||||
|
||||
_LOGGER = logging.getLogger(_PACKAGE)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandLineInterfaceState:
|
||||
args: argparse.Namespace
|
||||
texts: typing.Optional[typing.Iterable[str]] = None
|
||||
mark_writer: typing.Optional[typing.TextIO] = None
|
||||
tts: typing.Optional["Mimic3TextToSpeechSystem"] = None
|
||||
|
||||
all_audio: bytes = field(default_factory=bytes)
|
||||
sample_rate_hz: int = 22050
|
||||
sample_width_bytes: int = 2
|
||||
num_channels: int = 1
|
||||
|
||||
raw_queue: typing.Optional["Queue[bytes]"] = None
|
||||
raw_stream_thread: typing.Optional[threading.Thread] = None
|
||||
|
||||
|
||||
class OutputNaming(str, Enum):
|
||||
"""Format used for output file names"""
|
||||
|
||||
TEXT = "text"
|
||||
TIME = "time"
|
||||
ID = "id"
|
||||
|
||||
|
||||
class StdinFormat(str, Enum):
|
||||
"""Format of standard input"""
|
||||
|
||||
AUTO = "auto"
|
||||
"""Choose based on SSML state"""
|
||||
|
||||
LINES = "lines"
|
||||
"""Each line is a separate sentence/document"""
|
||||
|
||||
DOCUMENT = "document"
|
||||
"""Entire input is one document"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
args = get_args()
|
||||
|
||||
# TODO: Print version
|
||||
|
||||
# TODO: CUDA support
|
||||
# if args.cuda:
|
||||
# import torch
|
||||
|
||||
# args.cuda = torch.cuda.is_available()
|
||||
# if not args.cuda:
|
||||
# args.half = False
|
||||
# _LOGGER.warning("CUDA is not available")
|
||||
|
||||
# TODO: Disable Onnx optimizations
|
||||
# Handle optimizations.
|
||||
# onnxruntime crashes on armv7l if optimizations are enabled.
|
||||
# setattr(args, "no_optimizations", False)
|
||||
# if args.optimizations == "off":
|
||||
# args.no_optimizations = True
|
||||
# elif args.optimizations == "auto":
|
||||
# if platform.machine() == "armv7l":
|
||||
# # Enabling optimizations on 32-bit ARM crashes
|
||||
# args.no_optimizations = True
|
||||
|
||||
# TODO: Backend selection
|
||||
# backend: typing.Optional[InferenceBackend] = None
|
||||
# if args.backend:
|
||||
# backend = InferenceBackend(args.backend)
|
||||
|
||||
state = CommandLineInterfaceState(args=args)
|
||||
initialize_args(state)
|
||||
initialize_tts(state)
|
||||
|
||||
try:
|
||||
process_lines(state)
|
||||
finally:
|
||||
shutdown_tts(state)
|
||||
|
||||
|
||||
def initialize_args(state: CommandLineInterfaceState):
|
||||
import numpy as np
|
||||
|
||||
args = state.args
|
||||
|
||||
# Create output directory
|
||||
if args.output_dir:
|
||||
args.output_dir = Path(args.output_dir)
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Open file for writing the names from <mark> tags in SSML.
|
||||
# Each name is printed on a single line.
|
||||
if args.mark_file:
|
||||
args.mark_file = Path(args.mark_file)
|
||||
args.mark_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
state.mark_writer = open( # pylint: disable=consider-using-with
|
||||
args.mark_file, "w", encoding="utf-8"
|
||||
)
|
||||
else:
|
||||
state.mark_writer = sys.stderr
|
||||
|
||||
if args.seed is not None:
|
||||
_LOGGER.debug("Setting random seed to %s", args.seed)
|
||||
np.random.seed(args.seed)
|
||||
|
||||
if args.csv:
|
||||
args.output_naming = "id"
|
||||
|
||||
# Read text from stdin or arguments
|
||||
if args.text:
|
||||
# Use arguments
|
||||
state.texts = args.text
|
||||
else:
|
||||
# Use stdin
|
||||
stdin_format = StdinFormat.LINES
|
||||
|
||||
if (args.stdin_format == StdinFormat.AUTO) and args.ssml:
|
||||
# Assume SSML input is entire document
|
||||
stdin_format = StdinFormat.DOCUMENT
|
||||
|
||||
if stdin_format == StdinFormat.DOCUMENT:
|
||||
# One big line
|
||||
state.texts = [sys.stdin.read()]
|
||||
else:
|
||||
# Multiple lines
|
||||
state.texts = sys.stdin
|
||||
|
||||
if os.isatty(sys.stdin.fileno()):
|
||||
print("Reading text from stdin...", file=sys.stderr)
|
||||
|
||||
assert state.texts is not None
|
||||
|
||||
if args.process_on_blank_line:
|
||||
|
||||
# Combine text until a blank line is encountered.
|
||||
# Good for line-wrapped books where
|
||||
# sentences are broken
|
||||
# up across multiple
|
||||
# lines.
|
||||
def process_on_blank_line(lines: typing.Iterable[str]):
|
||||
text = ""
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if text:
|
||||
yield text
|
||||
|
||||
text = ""
|
||||
continue
|
||||
|
||||
text += " " + line
|
||||
|
||||
state.texts = process_on_blank_line(state.texts)
|
||||
|
||||
|
||||
def initialize_tts(state: CommandLineInterfaceState):
|
||||
import numpy as np
|
||||
from mimic3_tts import (
|
||||
Mimic3TextToSpeechSystem,
|
||||
Mimic3Settings,
|
||||
AudioResult,
|
||||
MarkResult,
|
||||
)
|
||||
|
||||
args = state.args
|
||||
|
||||
# TODO: voice/speaker
|
||||
state.tts = Mimic3TextToSpeechSystem(Mimic3Settings())
|
||||
|
||||
# max_thread_workers: typing.Optional[int] = None
|
||||
|
||||
# if args.max_thread_workers is not None:
|
||||
# max_thread_workers = (
|
||||
# None if args.max_thread_workers < 1 else args.max_thread_workers
|
||||
# )
|
||||
# elif args.raw_stream:
|
||||
# # Faster time to first audio
|
||||
# max_thread_workers = 2
|
||||
|
||||
# executor = ThreadPoolExecutor(max_workers=max_thread_workers)
|
||||
|
||||
# if os.isatty(sys.stdout.fileno()):
|
||||
# if (not args.output_dir) and (not args.raw_stream):
|
||||
# # No where else for the audio to go
|
||||
# args.interactive = True
|
||||
|
||||
if args.raw_stream:
|
||||
# Output in a separate thread to avoid blocking audio processing
|
||||
state.raw_queue = Queue(maxsize=args.raw_stream_queue_size)
|
||||
|
||||
def output_raw_stream():
|
||||
while True:
|
||||
audio = state.raw_queue.get()
|
||||
if audio is None:
|
||||
break
|
||||
|
||||
_LOGGER.debug(
|
||||
"Writing %s byte(s) of 16-bit 22050Hz mono PCM to stdout",
|
||||
len(audio),
|
||||
)
|
||||
sys.stdout.buffer.write(audio)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
state.raw_stream_thread = threading.Thread(
|
||||
target=output_raw_stream, daemon=True
|
||||
)
|
||||
state.raw_stream_thread.start()
|
||||
|
||||
# all_audios: typing.List[np.ndarray] = []
|
||||
# sample_rate: int = 22050
|
||||
# wav_data: typing.Optional[bytes] = None
|
||||
# play_command = shlex.split(args.play_command)
|
||||
|
||||
# # Settings for TTS and vocoder
|
||||
# tts_settings: typing.Dict[str, typing.Any] = {
|
||||
# "noise_scale": args.noise_scale,
|
||||
# "length_scale": args.length_scale,
|
||||
# }
|
||||
# vocoder_settings: typing.Dict[str, typing.Any] = {
|
||||
# "denoiser_strength": args.denoiser_strength,
|
||||
# }
|
||||
|
||||
|
||||
def process_line(line_id: str, line: str, state: CommandLineInterfaceState):
|
||||
from mimic3_tts import AudioResult, MarkResult
|
||||
|
||||
args = state.args
|
||||
assert state.tts is not None
|
||||
|
||||
# TODO: SSML
|
||||
state.tts.begin_utterance()
|
||||
|
||||
# TODO: text language
|
||||
state.tts.speak_text(line)
|
||||
|
||||
# TODO: CSV
|
||||
text_id = ""
|
||||
result_idx = 0
|
||||
|
||||
for result in state.tts.end_utterance():
|
||||
if isinstance(result, AudioResult):
|
||||
if args.raw_stream:
|
||||
assert state.raw_queue is not None
|
||||
state.raw_queue.put(result.audio_bytes)
|
||||
elif args.interactive or args.output_dir:
|
||||
# Convert to WAV audio
|
||||
wav_bytes: typing.Optional[bytes] = None
|
||||
if args.interactive:
|
||||
if not wav_bytes:
|
||||
wav_bytes = result.to_wav_bytes()
|
||||
|
||||
# play_audio(wav_bytes)
|
||||
pass
|
||||
|
||||
if args.output_dir:
|
||||
if not wav_bytes:
|
||||
wav_bytes = result.to_wav_bytes()
|
||||
|
||||
# Determine file name
|
||||
if args.output_naming == OutputNaming.TEXT:
|
||||
# Use text itself
|
||||
file_name = line.strip().replace(" ", "_")
|
||||
file_name = file_name.translate(
|
||||
str.maketrans("", "", string.punctuation.replace("_", ""))
|
||||
)
|
||||
elif args.output_naming == OutputNaming.TIME:
|
||||
# Use timestamp
|
||||
file_name = str(time.time())
|
||||
elif args.output_naming == OutputNaming.ID:
|
||||
if not text_id:
|
||||
text_id = line_id
|
||||
else:
|
||||
text_id = f"{line_id}_{result_idx + 1}"
|
||||
|
||||
file_name = text_id
|
||||
|
||||
assert file_name, f"No file name for text: {line}"
|
||||
wav_path = args.output_dir / (file_name + ".wav")
|
||||
wav_path.write_bytes(wav_bytes)
|
||||
|
||||
_LOGGER.debug("Wrote %s", wav_path)
|
||||
else:
|
||||
# Combine all audio and output to stdout at the end
|
||||
state.all_audio += result.audio_bytes
|
||||
state.sample_rate_hz = result.sample_rate_hz
|
||||
state.sample_width_bytes = result.sample_width_bytes
|
||||
state.num_channels = result.num_channels
|
||||
|
||||
result_idx += 1
|
||||
elif isinstance(result, MarkResult):
|
||||
if state.mark_writer:
|
||||
print(result.name, file=state.mark_writer)
|
||||
|
||||
# text_id = ""
|
||||
|
||||
# for result_idx, result in enumerate(tts_results):
|
||||
# text = result.text
|
||||
|
||||
# # Write before marks
|
||||
# if result.marks_before and state.mark_writer:
|
||||
# for mark_name in result.marks_before:
|
||||
# print(mark_name, file=state.mark_writer)
|
||||
|
||||
# if args.raw_stream:
|
||||
# assert raw_queue is not None
|
||||
# raw_queue.put(result.audio.tobytes())
|
||||
# elif args.interactive or args.output_dir:
|
||||
# # Convert to WAV audio
|
||||
# with io.BytesIO() as wav_io:
|
||||
# wav_write(wav_io, result.sample_rate, result.audio)
|
||||
# wav_data = wav_io.getvalue()
|
||||
|
||||
# assert wav_data is not None
|
||||
|
||||
# if args.interactive:
|
||||
|
||||
# # Play audio
|
||||
# _LOGGER.debug("Playing audio with play command")
|
||||
# try:
|
||||
# subprocess.run(
|
||||
# play_command,
|
||||
# input=wav_data,
|
||||
# stdout=subprocess.DEVNULL,
|
||||
# stderr=subprocess.DEVNULL,
|
||||
# check=True,
|
||||
# )
|
||||
# except FileNotFoundError:
|
||||
# _LOGGER.error(
|
||||
# "Unable to play audio with command '%s'. set with --play-command or redirect stdout",
|
||||
# args.play_command,
|
||||
# )
|
||||
# with open("output.wav", "wb") as output_file:
|
||||
# output_file.write(wav_data)
|
||||
|
||||
# _LOGGER.warning("stdout not redirected. Wrote audio to output.wav.")
|
||||
|
||||
# else:
|
||||
# # Combine all audio and output to stdout at the end
|
||||
# all_audios.append(result.audio)
|
||||
|
||||
# # Write after marks
|
||||
# if result.marks_after and state.mark_writer:
|
||||
# for mark_name in result.marks_after:
|
||||
# print(mark_name, file=state.mark_writer)
|
||||
|
||||
|
||||
def process_lines(state: CommandLineInterfaceState):
|
||||
assert state.texts is not None
|
||||
|
||||
args = state.args
|
||||
start_time_to_first_audio = time.perf_counter()
|
||||
|
||||
try:
|
||||
for line in state.texts:
|
||||
line_id = ""
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if args.output_naming == OutputNaming.ID:
|
||||
# Line has the format id|text instead of just text
|
||||
line_id, line = line.split(args.id_delimiter, maxsplit=1)
|
||||
|
||||
process_line(line_id, line, state)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if state.raw_queue is not None:
|
||||
# Draw audio playback queue
|
||||
while not state.raw_queue.empty():
|
||||
state.raw_queue.get()
|
||||
finally:
|
||||
# Wait for raw stream to finish
|
||||
if state.raw_queue is not None:
|
||||
state.raw_queue.put(None)
|
||||
|
||||
if state.raw_stream_thread is not None:
|
||||
state.raw_stream_thread.join()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Write combined audio to stdout
|
||||
if state.all_audio:
|
||||
_LOGGER.debug("Writing WAV audio to stdout")
|
||||
|
||||
wav_file: wave.Wave_write = wave.open(sys.stdout.buffer, "wb")
|
||||
with wav_file:
|
||||
wav_file.setframerate(state.sample_rate_hz)
|
||||
wav_file.setsampwidth(state.sample_width_bytes)
|
||||
wav_file.setnchannels(state.num_channels)
|
||||
wav_file.writeframes(state.all_audio)
|
||||
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
|
||||
def shutdown_tts(state: CommandLineInterfaceState):
|
||||
if state.tts is not None:
|
||||
state.tts.shutdown()
|
||||
state.tts = None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_args():
|
||||
"""Parse command-line arguments"""
|
||||
parser = argparse.ArgumentParser(prog=_PACKAGE)
|
||||
# parser.add_argument(
|
||||
# "--language", help="Gruut language for text input (en-us, etc.)"
|
||||
# )
|
||||
parser.add_argument(
|
||||
"text", nargs="*", help="Text to convert to speech (default: stdin)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stdin-format",
|
||||
choices=[str(v.value) for v in StdinFormat],
|
||||
default=StdinFormat.AUTO,
|
||||
help="Format of stdin text (default: auto)",
|
||||
)
|
||||
# parser.add_argument(
|
||||
# "--voice",
|
||||
# "-v",
|
||||
# default="en-us",
|
||||
# help="Name of voice (expected in <voices-dir>/<language>)",
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# "--voices-dir",
|
||||
# help="Directory with voices (format is <language>/<name_model-type>)",
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# "--list", action="store_true", help="List available voices/vocoders"
|
||||
# )
|
||||
parser.add_argument("--output-dir", help="Directory to write WAV file(s)")
|
||||
parser.add_argument(
|
||||
"--output-naming",
|
||||
choices=[v.value for v in OutputNaming],
|
||||
default="text",
|
||||
help="Naming scheme for output WAV files (requires --output-dir)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--id-delimiter",
|
||||
default="|",
|
||||
help="Delimiter between id and text in lines (default: |). Requires --output-naming id",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interactive",
|
||||
action="store_true",
|
||||
help="Play audio after each input line (see --play-command)",
|
||||
)
|
||||
parser.add_argument("--csv", action="store_true", help="Input format is id|text")
|
||||
parser.add_argument(
|
||||
"--mark-file",
|
||||
help="File to write mark names to as they're encountered (--ssml only)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--noise-scale",
|
||||
type=float,
|
||||
default=0.333,
|
||||
help="Noise scale (default: 0.333)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--length-scale",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Length scale (default: 1.0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noise-w",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Variation in cadence (default: 1.0)",
|
||||
)
|
||||
|
||||
# Miscellaneous
|
||||
parser.add_argument(
|
||||
"--max-thread-workers",
|
||||
type=int,
|
||||
help="Maximum number of threads to concurrently load models and run sentences through TTS/Vocoder",
|
||||
)
|
||||
# parser.add_argument(
|
||||
# "--play-command",
|
||||
# default="play -",
|
||||
# help="Shell command used to play audio in interactive model (default: play -)",
|
||||
# )
|
||||
parser.add_argument(
|
||||
"--raw-stream",
|
||||
action="store_true",
|
||||
help="Stream raw 16-bit 22050Hz mono PCM audio to stdout",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--raw-stream-queue-size",
|
||||
default=5,
|
||||
help="Maximum number of sentences to maintain in output queue with --raw-stream (default: 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--process-on-blank-line",
|
||||
action="store_true",
|
||||
help="Process text only after encountering a blank line",
|
||||
)
|
||||
parser.add_argument("--ssml", action="store_true", help="Input text is SSML")
|
||||
# parser.add_argument("--cuda", action="store_true", help="Use CUDA if available")
|
||||
# parser.add_argument(
|
||||
# "--half",
|
||||
# action="store_true",
|
||||
# help="Use faster FP16 for inference (requires --cuda)",
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# "--optimizations",
|
||||
# choices=["auto", "on", "off"],
|
||||
# default="auto",
|
||||
# help="Enable/disable Onnx optimizations (auto=disable on armv7l)",
|
||||
# )
|
||||
|
||||
# parser.add_argument(
|
||||
# "--backend",
|
||||
# choices=[v.value for v in InferenceBackend],
|
||||
# help="Force use of specific inference backend (default: prefer onnx)",
|
||||
# )
|
||||
|
||||
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(
|
||||
"--debug", action="store_true", help="Print DEBUG messages to the console"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# if args.version:
|
||||
# # Print version and exit
|
||||
# from larynx import __version__
|
||||
|
||||
# print(__version__)
|
||||
# sys.exit(0)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# # Directories to search for voices
|
||||
# voices_dirs = get_voices_dirs(args.voices_dir)
|
||||
|
||||
# def list_voices_vocoders():
|
||||
# """Print all vocoders and voices"""
|
||||
# # (type, name) -> location
|
||||
# local_info = {}
|
||||
|
||||
# # Search for downloaded voices/vocoders
|
||||
# for voices_dir in voices_dirs:
|
||||
# if not voices_dir.is_dir():
|
||||
# continue
|
||||
|
||||
# for voice_dir in voices_dir.iterdir():
|
||||
# if not voice_dir.is_dir():
|
||||
# continue
|
||||
|
||||
# if voice_dir.name in VOCODER_DIR_NAMES:
|
||||
# # Vocoder
|
||||
# for vocoder_model_dir in voice_dir.iterdir():
|
||||
# if not valid_voice_dir(vocoder_model_dir):
|
||||
# continue
|
||||
|
||||
# full_vocoder_name = f"{voice_dir.name}-{vocoder_model_dir.name}"
|
||||
# local_info[("vocoder", full_vocoder_name)] = str(
|
||||
# vocoder_model_dir
|
||||
# )
|
||||
# else:
|
||||
# # Voice
|
||||
# voice_lang = voice_dir.name
|
||||
# for voice_model_dir in voice_dir.iterdir():
|
||||
# if not valid_voice_dir(voice_model_dir):
|
||||
# continue
|
||||
|
||||
# local_info[("voice", voice_model_dir.name)] = str(
|
||||
# voice_model_dir
|
||||
# )
|
||||
|
||||
# # (type, lang, name, downloaded, aliases, location)
|
||||
# voices_and_vocoders = []
|
||||
# with open(_DIR / "VOCODERS", "r", encoding="utf-8") as vocoders_file:
|
||||
# for line in vocoders_file:
|
||||
# line = line.strip()
|
||||
# if not line:
|
||||
# continue
|
||||
|
||||
# *vocoder_aliases, full_vocoder_name = line.split()
|
||||
# downloaded = False
|
||||
|
||||
# location = local_info.get(("vocoder", full_vocoder_name), "")
|
||||
# if location:
|
||||
# downloaded = True
|
||||
|
||||
# voices_and_vocoders.append(
|
||||
# (
|
||||
# "vocoder",
|
||||
# " ",
|
||||
# "*" if downloaded else " ",
|
||||
# full_vocoder_name,
|
||||
# ",".join(vocoder_aliases),
|
||||
# location,
|
||||
# )
|
||||
# )
|
||||
|
||||
# with open(_DIR / "VOICES", "r", encoding="utf-8") as voices_file:
|
||||
# for line in voices_file:
|
||||
# line = line.strip()
|
||||
# if not line:
|
||||
# continue
|
||||
|
||||
# *voice_aliases, full_voice_name, download_name = line.split()
|
||||
# voice_lang = download_name.split("_", maxsplit=1)[0]
|
||||
|
||||
# downloaded = False
|
||||
|
||||
# location = local_info.get(("voice", full_voice_name), "")
|
||||
# if location:
|
||||
# downloaded = True
|
||||
|
||||
# voices_and_vocoders.append(
|
||||
# (
|
||||
# "voice",
|
||||
# voice_lang,
|
||||
# "*" if downloaded else " ",
|
||||
# full_voice_name,
|
||||
# ",".join(voice_aliases),
|
||||
# location,
|
||||
# )
|
||||
# )
|
||||
|
||||
# headers = ("TYPE", "LANG", "LOCAL", "NAME", "ALIASES", "LOCATION")
|
||||
|
||||
# # Get widths of columns
|
||||
# col_widths = [0] * len(voices_and_vocoders[0])
|
||||
# for item in voices_and_vocoders:
|
||||
# for col in range(len(col_widths)):
|
||||
# col_widths[col] = max(
|
||||
# col_widths[col], len(item[col]) + 1, len(headers[col]) + 1
|
||||
# )
|
||||
|
||||
# # Print results
|
||||
# print(*(h.ljust(col_widths[col]) for col, h in enumerate(headers)))
|
||||
|
||||
# for item in sorted(voices_and_vocoders):
|
||||
# print(*(v.ljust(col_widths[col]) for col, v in enumerate(item)))
|
||||
|
||||
# if args.list:
|
||||
# list_voices_vocoders()
|
||||
# sys.exit(0)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
4
mimic3-cli/mypy.ini
Normal file
4
mimic3-cli/mypy.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-setuptools.*]
|
||||
ignore_missing_imports = True
|
||||
39
mimic3-cli/pylintrc
Normal file
39
mimic3-cli/pylintrc
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
format,
|
||||
abstract-class-little-used,
|
||||
abstract-method,
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
global-statement,
|
||||
import-outside-toplevel,
|
||||
inconsistent-return-statements,
|
||||
locally-disabled,
|
||||
not-context-manager,
|
||||
redefined-variable-type,
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
too-many-boolean-expressions,
|
||||
unnecessary-pass,
|
||||
unused-argument,
|
||||
broad-except,
|
||||
too-many-nested-blocks,
|
||||
invalid-name,
|
||||
unused-import,
|
||||
no-self-use,
|
||||
fixme,
|
||||
useless-super-delegation,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
import-error
|
||||
|
||||
[FORMAT]
|
||||
expected-line-ending-format=LF
|
||||
1
mimic3-cli/requirements.txt
Normal file
1
mimic3-cli/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
mimic3-tts<1.0
|
||||
7
mimic3-cli/requirements_dev.txt
Normal file
7
mimic3-cli/requirements_dev.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
22
mimic3-cli/setup.cfg
Normal file
22
mimic3-cli/setup.cfg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[flake8]
|
||||
# To work with Black
|
||||
max-line-length = 88
|
||||
# E501: line too long
|
||||
# W503: Line break occurred before a binary operator
|
||||
# E203: Whitespace before ':'
|
||||
# D202 No blank lines allowed after function docstring
|
||||
# W504 line break after binary operator
|
||||
ignore =
|
||||
E501,
|
||||
W503,
|
||||
E203,
|
||||
D202,
|
||||
W504
|
||||
|
||||
[isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
indent = " "
|
||||
54
mimic3-cli/setup.py
Normal file
54
mimic3-cli/setup.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
from setuptools import setup
|
||||
|
||||
this_dir = Path(__file__).parent
|
||||
module_dir = this_dir / "mimic3"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Load README in as long description
|
||||
long_description: str = ""
|
||||
readme_path = this_dir / "README.md"
|
||||
if readme_path.is_file():
|
||||
long_description = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
requirements = []
|
||||
requirements_path = this_dir / "requirements.txt"
|
||||
if requirements_path.is_file():
|
||||
with open(requirements_path, "r", encoding="utf-8") as requirements_file:
|
||||
requirements = requirements_file.read().splitlines()
|
||||
|
||||
version_path = module_dir / "VERSION"
|
||||
with open(version_path, "r", encoding="utf-8") as version_file:
|
||||
version = version_file.read().strip()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
PLUGIN_ENTRY_POINT = "mimic3_tts_plug = mimic3.plugin:Mimic3TTSPlugin"
|
||||
setup(
|
||||
name="mimic3",
|
||||
version=version,
|
||||
description="An offline text to speech system for Mycroft",
|
||||
url="http://github.com/MycroftAI/mimic3",
|
||||
author="Michael Hansen",
|
||||
author_email="michael.hansen@mycroft.ai",
|
||||
license="Apache-2.0",
|
||||
packages=setuptools.find_packages(),
|
||||
package_data={"mimic3": ["VERSION", "py.typed", "templates", "css", "img"]},
|
||||
install_requires=requirements,
|
||||
extras_require={':python_version<"3.9"': ["importlib_resources"]},
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
keywords="mycroft plugin tts mimic",
|
||||
entry_points={"mycroft.plugin.tts": PLUGIN_ENTRY_POINT},
|
||||
)
|
||||
14
mimic3-http/.gitignore
vendored
Normal file
14
mimic3-http/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
6
mimic3-http/.isort.cfg
Normal file
6
mimic3-http/.isort.cfg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
3
mimic3-http/.projectile
Normal file
3
mimic3-http/.projectile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- /.venv/
|
||||
- /.mypy_cache/
|
||||
- /mimic3_http/.mypy_cache/
|
||||
201
mimic3-http/LICENSE
Normal file
201
mimic3-http/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
mimic3-http/MANIFEST.in
Normal file
5
mimic3-http/MANIFEST.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
include requirements.txt
|
||||
include requirements_dev.txt
|
||||
include LICENSE
|
||||
include README.md
|
||||
include mimic3_tts/VERSION
|
||||
28
mimic3-http/check.sh
Executable file
28
mimic3-http/check.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# Directory of *this* script
|
||||
this_dir="$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
# Kebab to snake case
|
||||
module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')"
|
||||
src_dir="${this_dir}/${module_name}"
|
||||
|
||||
# Path to virtual environment
|
||||
: "${venv:=${this_dir}/.venv}"
|
||||
|
||||
if [ -d "${venv}" ]; then
|
||||
# Activate virtual environment if available
|
||||
source "${venv}/bin/activate"
|
||||
fi
|
||||
|
||||
# Format code
|
||||
black "${src_dir}"
|
||||
isort "${src_dir}"
|
||||
|
||||
# Check
|
||||
flake8 "${src_dir}"
|
||||
pylint "${src_dir}"
|
||||
mypy "${src_dir}"
|
||||
|
||||
echo 'OK'
|
||||
34
mimic3-http/install.sh
Executable file
34
mimic3-http/install.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/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"
|
||||
1
mimic3-http/mimic3_http/__init__.py
Normal file
1
mimic3-http/mimic3_http/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/env python3
|
||||
364
mimic3-http/mimic3_http/__main__.py
Normal file
364
mimic3-http/mimic3_http/__main__.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2022 Mycroft AI Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import io
|
||||
import wave
|
||||
import tempfile
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs
|
||||
from uuid import uuid4
|
||||
|
||||
import hypercorn
|
||||
import quart_cors
|
||||
from quart import (
|
||||
Quart,
|
||||
Response,
|
||||
jsonify,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
)
|
||||
|
||||
from mimic3_tts import Mimic3TextToSpeechSystem, Mimic3Settings, AudioResult
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MISSING = object()
|
||||
_TEMP_DIR: typing.Optional[Path] = None
|
||||
|
||||
_PACKAGE = "mimic3_http"
|
||||
_DIR = Path(__file__).parent
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
parser = argparse.ArgumentParser(prog=_PACKAGE)
|
||||
parser.add_argument(
|
||||
"--voices-dir",
|
||||
action="append",
|
||||
help="Directory with <language>/<voice> structure",
|
||||
)
|
||||
parser.add_argument("--voice", help="Default voice (name of model directory)")
|
||||
parser.add_argument(
|
||||
"--host", default="0.0.0.0", help="Host of HTTP server (default: 0.0.0.0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=59125, help="Port of HTTP server (default: 59125)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speaker-id", type=int, default=0, help="Default speaker id to use"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--length-scale", type=float, default=1.0, help="Speed of speech (> 1 is slower)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noise-scale", type=float, default=0.333, help="Noise source for audio (0-1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noise-w", type=float, default=1.0, help="Variation in cadence (0-1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cache-dir",
|
||||
nargs="?",
|
||||
default=_MISSING,
|
||||
help="Enable WAV cache with optional directory (default: no cache)",
|
||||
)
|
||||
# parser.add_argument(
|
||||
# "--max-loaded-models",
|
||||
# type=int,
|
||||
# default=0,
|
||||
# help="Maximum number of voice models that can be loaded simultaneously (0 for no limit)",
|
||||
# )
|
||||
parser.add_argument(
|
||||
"--debug", action="store_true", help="Print DEBUG messages to console"
|
||||
)
|
||||
# parser.add_argument(
|
||||
# "--version", action="store_true", help="Print version to console and exit"
|
||||
# )
|
||||
args = parser.parse_args()
|
||||
|
||||
# if args.version:
|
||||
# print(__version__)
|
||||
# sys.exit(0)
|
||||
|
||||
if args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
_LOGGER.debug(args)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True) # must be hashable
|
||||
class TextToWavParams:
|
||||
text: str
|
||||
voice: str = args.voice
|
||||
speaker_id: int = args.speaker_id
|
||||
noise_scale: float = args.noise_scale
|
||||
noise_w: float = args.noise_w
|
||||
length_scale: float = args.length_scale
|
||||
ssml: bool = False
|
||||
text_language: typing.Optional[str] = None
|
||||
|
||||
|
||||
# params -> Path
|
||||
_WAV_CACHE: typing.Dict[TextToWavParams, Path] = {}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# _TTS: typing.Dict[str, Mimic3] = {}
|
||||
# _VOICE: str = args.voice
|
||||
|
||||
|
||||
# TODO: XDG voice directories
|
||||
# TODO: args.voices_dir
|
||||
# TODO: Preload voice
|
||||
|
||||
mimic3 = Mimic3TextToSpeechSystem(
|
||||
Mimic3Settings(
|
||||
voice=args.voice,
|
||||
speaker_id=args.speaker_id,
|
||||
length_scale=args.length_scale,
|
||||
noise_scale=args.noise_scale,
|
||||
noise_w=args.noise_w,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def text_to_wav(params: TextToWavParams, no_cache: bool = False) -> bytes:
|
||||
|
||||
_LOGGER.debug(params)
|
||||
|
||||
if _TEMP_DIR and (not no_cache):
|
||||
# Look up in cache
|
||||
maybe_wav_path = _TEMP_DIR / f"{hash(params)}.wav"
|
||||
if maybe_wav_path.is_file():
|
||||
_LOGGER.debug("Loading WAV from cache: %s", maybe_wav_path)
|
||||
wav_bytes = maybe_wav_path.read_bytes()
|
||||
return wav_bytes
|
||||
|
||||
mimic3.voice = params.voice
|
||||
mimic3.speaker_id = params.speaker_id
|
||||
|
||||
mimic3.settings.length_scale = params.length_scale
|
||||
mimic3.settings.noise_scale = params.noise_scale
|
||||
mimic3.settings.noise_w = params.noise_w
|
||||
|
||||
with io.BytesIO() as wav_io:
|
||||
wav_file: wave.Wave_write = wave.open(wav_io, "wb")
|
||||
wav_params_set = False
|
||||
|
||||
with wav_file:
|
||||
# TODO: SSML
|
||||
mimic3.begin_utterance()
|
||||
mimic3.speak_text(params.text, text_language=params.text_language)
|
||||
results = mimic3.end_utterance()
|
||||
|
||||
for result in results:
|
||||
# TODO: Marks
|
||||
if isinstance(result, AudioResult):
|
||||
if not wav_params_set:
|
||||
wav_file.setframerate(result.sample_rate_hz)
|
||||
wav_file.setsampwidth(result.sample_width_bytes)
|
||||
wav_file.setnchannels(result.num_channels)
|
||||
wav_params_set = True
|
||||
|
||||
wav_file.writeframes(result.audio_bytes)
|
||||
|
||||
return wav_io.getvalue()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_TEMPLATES_DIR = _DIR / "templates"
|
||||
|
||||
app = Quart(_PACKAGE, template_folder=str(_TEMPLATES_DIR))
|
||||
app.secret_key = str(uuid4())
|
||||
|
||||
if args.debug:
|
||||
app.config["TEMPLATES_AUTO_RELOAD"] = True
|
||||
|
||||
app = quart_cors.cors(app)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_CSS_DIR = _DIR / "css"
|
||||
_IMG_DIR = _DIR / "img"
|
||||
|
||||
|
||||
def _to_bool(s: str) -> bool:
|
||||
return s.strip().lower() in {"true", "1", "yes", "on"}
|
||||
|
||||
|
||||
@app.route("/img/<path:filename>", methods=["GET"])
|
||||
async def img(filename) -> Response:
|
||||
"""Image static endpoint."""
|
||||
return await send_from_directory(_IMG_DIR, filename)
|
||||
|
||||
|
||||
@app.route("/css/<path:filename>", methods=["GET"])
|
||||
async def css(filename) -> Response:
|
||||
"""CSS static endpoint."""
|
||||
return await send_from_directory(_CSS_DIR, filename)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def app_index():
|
||||
"""Main page."""
|
||||
return await render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/api/tts", methods=["GET", "POST"])
|
||||
async def app_tts() -> Response:
|
||||
"""Speak text to WAV."""
|
||||
tts_args: typing.Dict[str, typing.Any] = {}
|
||||
|
||||
_LOGGER.debug(request.args)
|
||||
|
||||
voice = request.args.get("voice")
|
||||
if voice is not None:
|
||||
tts_args["voice"] = str(voice)
|
||||
|
||||
speaker_id = request.args.get("speakerId")
|
||||
if speaker_id is not None:
|
||||
tts_args["speaker_id"] = int(speaker_id)
|
||||
|
||||
# TTS settings
|
||||
noise_scale = request.args.get("noiseScale")
|
||||
if noise_scale is not None:
|
||||
tts_args["noise_scale"] = float(noise_scale)
|
||||
|
||||
noise_w = request.args.get("noiseW")
|
||||
if noise_w is not None:
|
||||
tts_args["noise_w"] = float(noise_w)
|
||||
|
||||
length_scale = request.args.get("lengthScale")
|
||||
if length_scale is not None:
|
||||
tts_args["length_scale"] = float(length_scale)
|
||||
|
||||
ssml_str = request.args.get("ssml")
|
||||
if ssml_str is not None:
|
||||
tts_args["ssml"] = _to_bool(ssml_str)
|
||||
|
||||
text_language = request.args.get("textLanguage")
|
||||
if text_language is not None:
|
||||
tts_args["text_language"] = str(text_language)
|
||||
|
||||
# Text can come from POST body or GET ?text arg
|
||||
if request.method == "POST":
|
||||
text = (await request.data).decode()
|
||||
else:
|
||||
text = request.args.get("text", "")
|
||||
|
||||
assert text, "No text provided"
|
||||
|
||||
# Cache settings
|
||||
no_cache_str = request.args.get("noCache", "")
|
||||
no_cache = _to_bool(no_cache_str)
|
||||
|
||||
wav_bytes = text_to_wav(TextToWavParams(text=text, **tts_args), no_cache=no_cache)
|
||||
|
||||
return Response(wav_bytes, mimetype="audio/wav")
|
||||
|
||||
|
||||
@app.route("/api/voices", methods=["GET"])
|
||||
async def api_voices():
|
||||
voices = mimic3.get_voices()
|
||||
voice_ids = sorted([v.name for v in voices])
|
||||
return jsonify(voice_ids)
|
||||
|
||||
|
||||
@app.route("/process", methods=["GET", "POST"])
|
||||
async def api_process():
|
||||
"""MaryTTS-compatible /process endpoint"""
|
||||
voice = args.voice
|
||||
|
||||
if request.method == "POST":
|
||||
data = parse_qs((await request.data).decode())
|
||||
text = data.get("INPUT_TEXT", [""])[0]
|
||||
|
||||
if "VOICE" in data:
|
||||
voice = str(data.get("VOICE", [voice])[0]).strip()
|
||||
else:
|
||||
text = request.args.get("INPUT_TEXT", "")
|
||||
voice = str(request.args.get("VOICE", voice)).strip()
|
||||
|
||||
voice = voice or args.voice
|
||||
|
||||
speaker_id = args.speaker_id
|
||||
if "#" in voice:
|
||||
voice, speaker_id_str = voice.split("#", maxsplit=1)
|
||||
speaker_id = int(speaker_id_str)
|
||||
|
||||
# Assume SSML if text begins with an angle bracket
|
||||
ssml = text.strip().startswith("<")
|
||||
|
||||
_LOGGER.debug("Speaking with voice '%s (speaker=%s)': %s", voice, speaker_id, text)
|
||||
wav_bytes = text_to_wav(
|
||||
TextToWavParams(
|
||||
text=text,
|
||||
voice=voice,
|
||||
speaker_id=speaker_id,
|
||||
ssml=ssml,
|
||||
length_scale=args.length_scale,
|
||||
noise_scale=args.noise_scale,
|
||||
noise_w=args.noise_w,
|
||||
)
|
||||
)
|
||||
|
||||
return Response(wav_bytes, mimetype="audio/wav")
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
async def handle_error(err) -> typing.Tuple[str, int]:
|
||||
"""Return error as text."""
|
||||
_LOGGER.exception(err)
|
||||
return (f"{err.__class__.__name__}: {err}", 500)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Run Web Server
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_LOGGER.info("Starting web server")
|
||||
|
||||
hyp_config = hypercorn.config.Config()
|
||||
hyp_config.bind = [f"{args.host}:{args.port}"]
|
||||
|
||||
with mimic3, tempfile.TemporaryDirectory(prefix="mimic3") as temp_dir:
|
||||
if args.cache_dir != _MISSING:
|
||||
if args.cache_dir is None:
|
||||
# Use temporary directory
|
||||
_TEMP_DIR = Path(temp_dir)
|
||||
else:
|
||||
# Use user-supplied cache directory
|
||||
_TEMP_DIR = Path(args.cache_dir)
|
||||
_TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if _TEMP_DIR:
|
||||
_LOGGER.debug("Cache directory: %s", _TEMP_DIR)
|
||||
|
||||
asyncio.run(hypercorn.asyncio.serve(app, hyp_config))
|
||||
7
mimic3-http/mimic3_http/css/bootstrap.min.css
vendored
Normal file
7
mimic3-http/mimic3_http/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
mimic3-http/mimic3_http/img/Mimic_color.png
Normal file
BIN
mimic3-http/mimic3_http/img/Mimic_color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
mimic3-http/mimic3_http/img/Mycroft_logo_two_typeonly.png
Normal file
BIN
mimic3-http/mimic3_http/img/Mycroft_logo_two_typeonly.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
208
mimic3-http/mimic3_http/templates/index.html
Normal file
208
mimic3-http/mimic3_http/templates/index.html
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="Mimic 3 text to speech server">
|
||||
<meta name="author" content="Michael Hansen">
|
||||
|
||||
<title>Mimic 3</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom styles for this template -->
|
||||
<style>
|
||||
body {
|
||||
padding-top: 0;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
body {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#mimic-logo {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
#mycroft-logo {
|
||||
height: 2rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Page Content -->
|
||||
<div id="main" class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h1>
|
||||
<img id="mimic-logo" src="img/Mimic_color.png" />
|
||||
Mimic 3
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<textarea id="text" placeholder="Type here..." class="form-control" rows="3" name="text" alt="Text to generate speech from"></textarea>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button id="speak-button" name="speak" class="btn btn-lg btn-primary" alt="Generate speech">Speak</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-auto">
|
||||
<label for="voice-list" title="Voice name">Voice:</label>
|
||||
<select id="voice-list" name="voices">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label for="speaker-id" title="Index of speaker">Speaker:</label>
|
||||
<input type="number" id="speaker-id" name="speaker_id" size="5" min="0" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div id="audio-message" class="row mt-3" hidden>
|
||||
<div class="col">
|
||||
<audio id="audio" preload="none" controls autoplay hidden></audio>
|
||||
<p id="message"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-auto">
|
||||
<label for="noise-scale" title="Voice volatility">Noise:</label>
|
||||
<input type="number" id="noise-scale" name="noiseScale" size="5" min="0" max="1" step="0.001" value="0.333">
|
||||
<label for="noise-w" class="ml-2" title="Voice volatility 2">Noise W:</label>
|
||||
<input type="number" id="noise-w" name="noiseW" size="5" min="0" max="1" step="0.001" value="1.0">
|
||||
<label for="length-scale" class="ml-2" title="Voice speed (< 1 is faster)">Length:</label>
|
||||
<input type="number" id="length-scale" name="lengthScale" size="5" min="0" step="0.001" value="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-auto">
|
||||
<label for="text-language" title="Text Language">Text Language:</label>
|
||||
<input type="text" id="text-language" name="textLanguage" size="8" placeholder="lang code">
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-5" />
|
||||
<div class="row mt-5">
|
||||
<img id="mycroft-logo" src="img/Mycroft_logo_two_typeonly.png" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap core JavaScript -->
|
||||
<script>
|
||||
var voicesInfo = {}
|
||||
|
||||
function q(selector) {return document.querySelector(selector)}
|
||||
q('#text').focus()
|
||||
|
||||
function do_tts(e) {
|
||||
text = q('#text').value
|
||||
if (text) {
|
||||
q('#message').textContent = 'Synthesizing...'
|
||||
q('#speak-button').disabled = true
|
||||
q('#audio').hidden = true
|
||||
synthesize(text)
|
||||
}
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
q('#speak-button').addEventListener('click', do_tts)
|
||||
|
||||
async function synthesize(text) {
|
||||
var voiceList = q('#voice-list')
|
||||
var voice = voiceList.options[voiceList.selectedIndex].value
|
||||
|
||||
var noiseScale = q('#noise-scale').value || '0.333'
|
||||
var noiseW = q('#noise-w').value || '1.0'
|
||||
var lengthScale = q('#length-scale').value || '1.0'
|
||||
var speakerId = q('#speaker-id').value || '0'
|
||||
var textLanguage = q('#text-language').value || ''
|
||||
|
||||
q('#audio-message').hidden = false
|
||||
|
||||
var startTime = performance.now()
|
||||
|
||||
res = await fetch(
|
||||
'api/tts?text=' + encodeURIComponent(text) +
|
||||
'&voice=' + encodeURIComponent(voice) +
|
||||
'&noiseScale=' + encodeURIComponent(noiseScale) +
|
||||
'&noiseW=' + encodeURIComponent(noiseW) +
|
||||
'&lengthScale=' + encodeURIComponent(lengthScale) +
|
||||
'&textLanguage=' + encodeURIComponent(textLanguage) +
|
||||
'&speakerId=' + encodeURIComponent(speakerId),
|
||||
{cache: 'no-cache'})
|
||||
|
||||
if (res.ok) {
|
||||
blob = await res.blob()
|
||||
var elapsedTime = performance.now() - startTime
|
||||
|
||||
q('#message').textContent = (elapsedTime / 1000) + ' second(s)'
|
||||
q('#speak-button').disabled = false
|
||||
q('#audio').src = URL.createObjectURL(blob)
|
||||
q('#audio').hidden = false
|
||||
} else {
|
||||
message = await res.text()
|
||||
q('#message').textContent = message
|
||||
q('#speak-button').disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
function voiceChanged() {
|
||||
var voiceList = q('#voice-list')
|
||||
|
||||
// Reset audio
|
||||
q('#audio-message').hidden = true
|
||||
q('#message').textContent = ''
|
||||
q('#audio').hidden = true
|
||||
q('#audio').autoplay = true
|
||||
}
|
||||
|
||||
q('#voice-list').addEventListener('change', voiceChanged)
|
||||
|
||||
function loadVoices() {
|
||||
voicesInfo = {}
|
||||
|
||||
// Remove previous voices
|
||||
var voiceList = q('#voice-list')
|
||||
for (var i = voiceList.options.length - 1; i >= 0; i--) {
|
||||
voiceList.options[i].remove()
|
||||
}
|
||||
|
||||
fetch('api/voices')
|
||||
.then(function(res) {
|
||||
if (!res.ok) throw Error(res.statusText)
|
||||
return res.json()
|
||||
}).then(function(voices) {
|
||||
voicesInfo = voices
|
||||
|
||||
// Populate select
|
||||
var indexToSelect = -1
|
||||
|
||||
voices.forEach(function(voice) {
|
||||
voiceList.insertAdjacentHTML(
|
||||
'beforeend', '<option value="' + voice + '">' + voice + '</option>'
|
||||
)
|
||||
})
|
||||
|
||||
voiceChanged()
|
||||
}).catch(function(err) {
|
||||
q('#message').textContent = 'Error: ' + err.message
|
||||
q('#speak-button').disabled = false
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
loadVoices()
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
mimic3-http/mypy.ini
Normal file
4
mimic3-http/mypy.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-setuptools.*]
|
||||
ignore_missing_imports = True
|
||||
39
mimic3-http/pylintrc
Normal file
39
mimic3-http/pylintrc
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
format,
|
||||
abstract-class-little-used,
|
||||
abstract-method,
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
global-statement,
|
||||
import-outside-toplevel,
|
||||
inconsistent-return-statements,
|
||||
locally-disabled,
|
||||
not-context-manager,
|
||||
redefined-variable-type,
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
too-many-boolean-expressions,
|
||||
unnecessary-pass,
|
||||
unused-argument,
|
||||
broad-except,
|
||||
too-many-nested-blocks,
|
||||
invalid-name,
|
||||
unused-import,
|
||||
no-self-use,
|
||||
fixme,
|
||||
useless-super-delegation,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
import-error
|
||||
|
||||
[FORMAT]
|
||||
expected-line-ending-format=LF
|
||||
3
mimic3-http/requirements.txt
Normal file
3
mimic3-http/requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mimic3-tts<1.0
|
||||
quart>=0.16,<1.0
|
||||
quart-cors
|
||||
7
mimic3-http/requirements_dev.txt
Normal file
7
mimic3-http/requirements_dev.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
22
mimic3-http/setup.cfg
Normal file
22
mimic3-http/setup.cfg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[flake8]
|
||||
# To work with Black
|
||||
max-line-length = 88
|
||||
# E501: line too long
|
||||
# W503: Line break occurred before a binary operator
|
||||
# E203: Whitespace before ':'
|
||||
# D202 No blank lines allowed after function docstring
|
||||
# W504 line break after binary operator
|
||||
ignore =
|
||||
E501,
|
||||
W503,
|
||||
E203,
|
||||
D202,
|
||||
W504
|
||||
|
||||
[isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
indent = " "
|
||||
54
mimic3-http/setup.py
Normal file
54
mimic3-http/setup.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
from setuptools import setup
|
||||
|
||||
this_dir = Path(__file__).parent
|
||||
module_dir = this_dir / "mimic3"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Load README in as long description
|
||||
long_description: str = ""
|
||||
readme_path = this_dir / "README.md"
|
||||
if readme_path.is_file():
|
||||
long_description = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
requirements = []
|
||||
requirements_path = this_dir / "requirements.txt"
|
||||
if requirements_path.is_file():
|
||||
with open(requirements_path, "r", encoding="utf-8") as requirements_file:
|
||||
requirements = requirements_file.read().splitlines()
|
||||
|
||||
version_path = module_dir / "VERSION"
|
||||
with open(version_path, "r", encoding="utf-8") as version_file:
|
||||
version = version_file.read().strip()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
PLUGIN_ENTRY_POINT = "mimic3_tts_plug = mimic3.plugin:Mimic3TTSPlugin"
|
||||
setup(
|
||||
name="mimic3",
|
||||
version=version,
|
||||
description="An offline text to speech system for Mycroft",
|
||||
url="http://github.com/MycroftAI/mimic3",
|
||||
author="Michael Hansen",
|
||||
author_email="michael.hansen@mycroft.ai",
|
||||
license="Apache-2.0",
|
||||
packages=setuptools.find_packages(),
|
||||
package_data={"mimic3": ["VERSION", "py.typed", "templates", "css", "img"]},
|
||||
install_requires=requirements,
|
||||
extras_require={':python_version<"3.9"': ["importlib_resources"]},
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
keywords="mycroft plugin tts mimic",
|
||||
entry_points={"mycroft.plugin.tts": PLUGIN_ENTRY_POINT},
|
||||
)
|
||||
14
mimic3-tts/.gitignore
vendored
Normal file
14
mimic3-tts/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
6
mimic3-tts/.isort.cfg
Normal file
6
mimic3-tts/.isort.cfg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
3
mimic3-tts/.projectile
Normal file
3
mimic3-tts/.projectile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- /.venv/
|
||||
- /.mypy_cache/
|
||||
- /mimic3_tts/.mypy_cache/
|
||||
201
mimic3-tts/LICENSE
Normal file
201
mimic3-tts/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
mimic3-tts/MANIFEST.in
Normal file
5
mimic3-tts/MANIFEST.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
include requirements.txt
|
||||
include requirements_dev.txt
|
||||
include LICENSE
|
||||
include README.md
|
||||
include mimic3_tts/VERSION
|
||||
28
mimic3-tts/check.sh
Executable file
28
mimic3-tts/check.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# Directory of *this* script
|
||||
this_dir="$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
# Kebab to snake case
|
||||
module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')"
|
||||
src_dir="${this_dir}/${module_name}"
|
||||
|
||||
# Path to virtual environment
|
||||
: "${venv:=${this_dir}/.venv}"
|
||||
|
||||
if [ -d "${venv}" ]; then
|
||||
# Activate virtual environment if available
|
||||
source "${venv}/bin/activate"
|
||||
fi
|
||||
|
||||
# Format code
|
||||
black "${src_dir}"
|
||||
isort "${src_dir}"
|
||||
|
||||
# Check
|
||||
flake8 "${src_dir}"
|
||||
pylint "${src_dir}"
|
||||
mypy "${src_dir}"
|
||||
|
||||
echo 'OK'
|
||||
34
mimic3-tts/install.sh
Executable file
34
mimic3-tts/install.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/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"
|
||||
1
mimic3-tts/mimic3_tts/VERSION
Normal file
1
mimic3-tts/mimic3_tts/VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.1.3
|
||||
2
mimic3-tts/mimic3_tts/__init__.py
Normal file
2
mimic3-tts/mimic3_tts/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .tts import Mimic3TextToSpeechSystem, Mimic3Settings
|
||||
from opentts_abc import AudioResult, MarkResult
|
||||
29
mimic3-tts/mimic3_tts/__main__.py
Normal file
29
mimic3-tts/mimic3_tts/__main__.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
import logging
|
||||
import wave
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
from opentts_abc.ssml import SSMLSpeaker
|
||||
from mimic3_tts.tts import Mimic3TextToSpeechSystem, Mimic3Settings, AudioResult, MarkResult
|
||||
|
||||
settings = Mimic3Settings(length_scale=1.2, noise_w=0)
|
||||
tts = Mimic3TextToSpeechSystem(settings)
|
||||
|
||||
speaker = SSMLSpeaker(tts)
|
||||
ssml = '<speak><s><voice name="en_US/vctk_low#20">This is a test.</voice></s></speak>'
|
||||
|
||||
wav_file: wave.Wave_write = wave.open("out.wav", "wb")
|
||||
params_set = False
|
||||
with wav_file:
|
||||
for result in speaker.speak(ssml):
|
||||
if isinstance(result, AudioResult):
|
||||
if not params_set:
|
||||
wav_file.setframerate(result.sample_rate_hz)
|
||||
wav_file.setsampwidth(result.sample_width_bytes)
|
||||
wav_file.setnchannels(result.num_channels)
|
||||
params_set = True
|
||||
|
||||
wav_file.writeframes(result.audio_bytes)
|
||||
elif isinstance(result, MarkResult):
|
||||
print("mark", result.name)
|
||||
274
mimic3-tts/mimic3_tts/config.py
Normal file
274
mimic3-tts/mimic3_tts/config.py
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
"""Configuration classes"""
|
||||
# Copyright 2021 Mycroft AI Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import collections
|
||||
import json
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from dataclasses_json import DataClassJsonMixin
|
||||
from gruut_ipa import IPA
|
||||
from phonemes2ids import BlankBetween
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioConfig(DataClassJsonMixin):
|
||||
filter_length: int = 1024
|
||||
hop_length: int = 256
|
||||
win_length: int = 1024
|
||||
mel_channels: int = 80
|
||||
sample_rate: int = 22050
|
||||
sample_bytes: int = 2
|
||||
channels: int = 1
|
||||
mel_fmin: float = 0.0
|
||||
mel_fmax: typing.Optional[float] = None
|
||||
ref_level_db: float = 20.0
|
||||
spec_gain: float = 1.0
|
||||
|
||||
# Normalization
|
||||
signal_norm: bool = True
|
||||
min_level_db: float = -100.0
|
||||
max_norm: float = 1.0
|
||||
clip_norm: bool = True
|
||||
symmetric_norm: bool = True
|
||||
do_dynamic_range_compression: bool = True
|
||||
convert_db_to_amp: bool = True
|
||||
|
||||
do_trim_silence: bool = False
|
||||
trim_silence_db: float = 40.0
|
||||
trim_margin_sec: float = 0.01
|
||||
trim_keep_sec: float = 0.25
|
||||
|
||||
scale_mels: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.mel_fmax is not None:
|
||||
assert self.mel_fmax <= self.sample_rate // 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelConfig(DataClassJsonMixin):
|
||||
num_symbols: int = 0
|
||||
n_speakers: int = 1
|
||||
|
||||
inter_channels: int = 192
|
||||
hidden_channels: int = 192
|
||||
filter_channels: int = 768
|
||||
n_heads: int = 2
|
||||
n_layers: int = 6
|
||||
kernel_size: int = 3
|
||||
p_dropout: float = 0.1
|
||||
resblock: str = "1"
|
||||
resblock_kernel_sizes: typing.Tuple[int, ...] = (3, 7, 11)
|
||||
resblock_dilation_sizes: typing.Tuple[typing.Tuple[int, ...], ...] = (
|
||||
(1, 3, 5),
|
||||
(1, 3, 5),
|
||||
(1, 3, 5),
|
||||
)
|
||||
upsample_rates: typing.Tuple[int, ...] = (8, 8, 2, 2)
|
||||
upsample_initial_channel: int = 512
|
||||
upsample_kernel_sizes: typing.Tuple[int, ...] = (16, 16, 4, 4)
|
||||
n_layers_q: int = 3
|
||||
use_spectral_norm: bool = False
|
||||
gin_channels: int = 256
|
||||
use_sdp: bool = True # StochasticDurationPredictor
|
||||
|
||||
@property
|
||||
def is_multispeaker(self) -> bool:
|
||||
return self.n_speakers > 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhonemesConfig(DataClassJsonMixin):
|
||||
phoneme_separator: str = " "
|
||||
"""Separator between individual phonemes in CSV input"""
|
||||
|
||||
word_separator: str = "#"
|
||||
"""Separator between word phonemes in CSV input (must not match phoneme_separator)"""
|
||||
|
||||
phoneme_to_id: typing.Optional[typing.Mapping[str, int]] = None
|
||||
pad: typing.Optional[str] = "_"
|
||||
bos: typing.Optional[str] = None
|
||||
eos: typing.Optional[str] = None
|
||||
blank: typing.Optional[str] = "#"
|
||||
blank_word: typing.Optional[str] = None
|
||||
blank_between: typing.Union[str, BlankBetween] = BlankBetween.WORDS
|
||||
blank_at_start: bool = True
|
||||
blank_at_end: bool = True
|
||||
simple_punctuation: bool = True
|
||||
punctuation_map: typing.Optional[typing.Mapping[str, str]] = None
|
||||
separate: typing.Optional[typing.List[str]] = None
|
||||
separate_graphemes: bool = False
|
||||
separate_tones: bool = False
|
||||
tone_before: bool = False
|
||||
phoneme_map: typing.Optional[typing.Mapping[str, str]] = None
|
||||
auto_bos_eos: bool = False
|
||||
minor_break: typing.Optional[str] = IPA.BREAK_MINOR.value
|
||||
major_break: typing.Optional[str] = IPA.BREAK_MAJOR.value
|
||||
|
||||
def split_word_phonemes(self, phonemes_str: str) -> typing.List[typing.List[str]]:
|
||||
"""Split phonemes string into a list of lists (outer is words, inner is individual phonemes in each word)"""
|
||||
return [
|
||||
word_phonemes_str.split(self.phoneme_separator)
|
||||
for word_phonemes_str in phonemes_str.split(self.word_separator)
|
||||
]
|
||||
|
||||
def join_word_phonemes(self, word_phonemes: typing.List[typing.List[str]]) -> str:
|
||||
"""Split phonemes string into a list of lists (outer is words, inner is individual phonemes in each word)"""
|
||||
return self.word_separator.join(
|
||||
self.phoneme_separator.join(wp) for wp in word_phonemes
|
||||
)
|
||||
|
||||
|
||||
class Phonemizer(str, Enum):
|
||||
SYMBOLS = "symbols"
|
||||
GRUUT = "gruut"
|
||||
ESPEAK = "espeak"
|
||||
|
||||
|
||||
class Aligner(str, Enum):
|
||||
KALDI_ALIGN = "kaldi_align"
|
||||
|
||||
|
||||
class TextCasing(str, Enum):
|
||||
LOWER = "lower"
|
||||
UPPER = "upper"
|
||||
|
||||
|
||||
class MetadataFormat(str, Enum):
|
||||
TEXT = "text"
|
||||
PHONEMES = "phonemes"
|
||||
PHONEME_IDS = "ids"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatasetConfig:
|
||||
name: str
|
||||
metadata_path: typing.Optional[typing.Union[str, Path]] = None
|
||||
train_path: typing.Optional[typing.Union[str, Path]] = None
|
||||
multispeaker: bool = False
|
||||
text_language: typing.Optional[str] = None
|
||||
audio_dir: typing.Optional[typing.Union[str, Path]] = None
|
||||
cache_dir: typing.Optional[typing.Union[str, Path]] = None
|
||||
|
||||
def get_cache_dir(self, output_dir: typing.Union[str, Path]) -> Path:
|
||||
if self.cache_dir is not None:
|
||||
cache_dir = Path(self.cache_dir)
|
||||
else:
|
||||
cache_dir = Path("cache") / self.name
|
||||
|
||||
if not cache_dir.is_absolute():
|
||||
cache_dir = Path(output_dir) / str(cache_dir)
|
||||
|
||||
return cache_dir
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlignerConfig:
|
||||
aligner: typing.Optional[Aligner] = None
|
||||
casing: typing.Optional[TextCasing] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainingConfig(DataClassJsonMixin):
|
||||
seed: int = 1234
|
||||
epochs: int = 10000
|
||||
learning_rate: float = 2e-4
|
||||
betas: typing.Tuple[float, float] = field(default=(0.8, 0.99))
|
||||
eps: float = 1e-9
|
||||
batch_size: int = 32
|
||||
fp16_run: bool = False
|
||||
lr_decay: float = 0.999875
|
||||
segment_size: int = 8192
|
||||
init_lr_ratio: float = 1.0
|
||||
warmup_epochs: int = 0
|
||||
c_mel: int = 45
|
||||
c_kl: float = 1.0
|
||||
grad_clip: typing.Optional[float] = None
|
||||
|
||||
min_seq_length: typing.Optional[int] = None
|
||||
max_seq_length: typing.Optional[int] = None
|
||||
|
||||
min_spec_length: typing.Optional[int] = None
|
||||
max_spec_length: typing.Optional[int] = None
|
||||
|
||||
last_epoch: int = 1
|
||||
global_step: int = 1
|
||||
best_loss: typing.Optional[float] = None
|
||||
audio: AudioConfig = field(default_factory=AudioConfig)
|
||||
model: ModelConfig = field(default_factory=ModelConfig)
|
||||
phonemes: PhonemesConfig = field(default_factory=PhonemesConfig)
|
||||
text_aligner: AlignerConfig = field(default_factory=AlignerConfig)
|
||||
text_language: typing.Optional[str] = None
|
||||
phonemizer: typing.Optional[Phonemizer] = None
|
||||
datasets: typing.List[DatasetConfig] = field(default_factory=list)
|
||||
dataset_format: MetadataFormat = MetadataFormat.TEXT
|
||||
|
||||
version: int = 1
|
||||
git_commit: str = ""
|
||||
|
||||
@property
|
||||
def is_multispeaker(self):
|
||||
return (
|
||||
self.model.is_multispeaker
|
||||
or any(d.multispeaker for d in self.datasets)
|
||||
)
|
||||
|
||||
def save(self, config_file: typing.TextIO):
|
||||
"""Save config as JSON to a file"""
|
||||
json.dump(self.to_dict(), config_file, indent=4)
|
||||
|
||||
@staticmethod
|
||||
def load(config_file: typing.TextIO) -> "TrainingConfig":
|
||||
"""Load config from a JSON file"""
|
||||
return TrainingConfig.from_json(config_file.read())
|
||||
|
||||
@staticmethod
|
||||
def load_and_merge(
|
||||
config: "TrainingConfig",
|
||||
config_files: typing.Iterable[typing.Union[str, Path, typing.TextIO]],
|
||||
) -> "TrainingConfig":
|
||||
"""Loads one or more JSON configuration files and overlays them on top of an existing config"""
|
||||
base_dict = config.to_dict()
|
||||
for maybe_config_file in config_files:
|
||||
if isinstance(maybe_config_file, (str, Path)):
|
||||
# File path
|
||||
config_file = open(maybe_config_file, "r", encoding="utf-8")
|
||||
else:
|
||||
# File object
|
||||
config_file = maybe_config_file
|
||||
|
||||
with config_file:
|
||||
# Load new config and overlay on existing config
|
||||
new_dict = json.load(config_file)
|
||||
TrainingConfig.recursive_update(base_dict, new_dict)
|
||||
|
||||
return TrainingConfig.from_dict(base_dict)
|
||||
|
||||
@staticmethod
|
||||
def recursive_update(
|
||||
base_dict: typing.Dict[typing.Any, typing.Any],
|
||||
new_dict: typing.Mapping[typing.Any, typing.Any],
|
||||
) -> None:
|
||||
"""Recursively overwrites values in base dictionary with values from new dictionary"""
|
||||
for key, value in new_dict.items():
|
||||
if isinstance(value, collections.Mapping) and (
|
||||
base_dict.get(key) is not None
|
||||
):
|
||||
TrainingConfig.recursive_update(base_dict[key], value)
|
||||
else:
|
||||
base_dict[key] = value
|
||||
0
mimic3-tts/mimic3_tts/py.typed
Normal file
0
mimic3-tts/mimic3_tts/py.typed
Normal file
808
mimic3-tts/mimic3_tts/tts.py
Normal file
808
mimic3-tts/mimic3_tts/tts.py
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
from abc import ABCMeta
|
||||
from dataclasses import dataclass, field
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape as xmlescape
|
||||
|
||||
import gruut
|
||||
import numpy as np
|
||||
import onnxruntime
|
||||
import phonemes2ids
|
||||
from gruut.const import LookupPhonemes, WordRole
|
||||
from gruut_ipa import guess_phonemes, IPA, Phonemes, Phoneme
|
||||
|
||||
from opentts_abc import (
|
||||
TextToSpeechSystem,
|
||||
Voice,
|
||||
BaseToken,
|
||||
BaseResult,
|
||||
MarkResult,
|
||||
AudioResult,
|
||||
Word,
|
||||
Phonemes,
|
||||
SayAs,
|
||||
)
|
||||
|
||||
from mimic3_tts.config import TrainingConfig
|
||||
from mimic3_tts.utils import audio_float_to_int16
|
||||
|
||||
_DIR = Path(__file__).parent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PHONEMES_LIST = typing.List[typing.List[str]]
|
||||
|
||||
DEFAULT_VOICE = "en_US/vctk_low"
|
||||
DEFAULT_LANGUAGE = "en_US"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mimic3Settings:
|
||||
voice: typing.Optional[str] = None
|
||||
language: typing.Optional[str] = None
|
||||
voices_directories: typing.Optional[typing.Iterable[typing.Union[str, Path]]] = None
|
||||
speaker_id: typing.Optional[int] = None
|
||||
length_scale: float = 1.0
|
||||
noise_scale: float = 0.333
|
||||
noise_w: float = 1.0
|
||||
text_language: typing.Optional[str] = None
|
||||
sample_rate: int = 22050
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadedVoice:
|
||||
config: TrainingConfig
|
||||
onnx_model: onnxruntime.InferenceSession
|
||||
phoneme_to_id: typing.Mapping[str, int]
|
||||
phoneme_map: typing.Optional[typing.Dict[str, typing.List[str]]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mimic3Phonemes:
|
||||
current_settings: Mimic3Settings
|
||||
phonemes: typing.List[typing.List[str]] = field(default_factory=list)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Mimic3TextToSpeechSystem(TextToSpeechSystem):
|
||||
"""Convert text to speech using Mimic 3"""
|
||||
|
||||
def __init__(self, settings: Mimic3Settings):
|
||||
self.settings = settings
|
||||
|
||||
# self._current_voice: typing.Optional[LoadedVoice] = None
|
||||
# self._current_settings = self.settings
|
||||
|
||||
self._results: typing.List[typing.Union[BaseResult, Mimic3Phonemes]] = []
|
||||
|
||||
self.loaded_voices: typing.Dict[str, LoadedVoice] = {}
|
||||
|
||||
@property
|
||||
def voice(self) -> str:
|
||||
return self.settings.voice or DEFAULT_VOICE
|
||||
|
||||
@voice.setter
|
||||
def voice(self, new_voice: str):
|
||||
if new_voice != self.settings.voice:
|
||||
# Clear speaker id on voice change
|
||||
self.speaker_id = None
|
||||
|
||||
self.settings.voice = new_voice
|
||||
|
||||
if "#" in self.settings.voice:
|
||||
# Split
|
||||
voice, speaker_id_str = self.settings.voice.split("#", maxsplit=1)
|
||||
self.settings.voice = voice
|
||||
|
||||
# TODO: Use speaker map
|
||||
self.speaker_id = int(speaker_id_str)
|
||||
|
||||
# self._current_voice = self._get_or_load_voice(
|
||||
# self.settings.voice or DEFAULT_VOICE
|
||||
# )
|
||||
|
||||
@property
|
||||
def speaker_id(self) -> typing.Optional[int]:
|
||||
return self.settings.speaker_id
|
||||
|
||||
@speaker_id.setter
|
||||
def speaker_id(self, new_speaker_id: typing.Optional[int]):
|
||||
self.settings.speaker_id = new_speaker_id
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
return self.settings.language or DEFAULT_LANGUAGE
|
||||
|
||||
@language.setter
|
||||
def language(self, new_language: str):
|
||||
self.settings.language = new_language
|
||||
|
||||
@staticmethod
|
||||
def get_default_voices_directories() -> typing.List[Path]:
|
||||
return [_DIR.parent.parent / "voices"]
|
||||
|
||||
# @property
|
||||
# def text_lang(self) -> str:
|
||||
# return (
|
||||
# self.settings.text_language
|
||||
# or self.settings.language
|
||||
# or (
|
||||
# self._current_voice.config.text_language
|
||||
# if self._current_voice
|
||||
# else None
|
||||
# )
|
||||
# or "en_US"
|
||||
# )
|
||||
|
||||
# @property
|
||||
# def sample_rate(self) -> int:
|
||||
# return (
|
||||
# self._current_voice.config.audio.sample_rate
|
||||
# if self._current_voice
|
||||
# else self.settings.sample_rate
|
||||
# )
|
||||
|
||||
def get_voices(self) -> typing.Iterable[Voice]:
|
||||
voices_dirs = (
|
||||
self.settings.voices_directories
|
||||
or Mimic3TextToSpeechSystem.get_default_voices_directories()
|
||||
)
|
||||
|
||||
# voices/<language>/<voice>/
|
||||
for voices_dir in voices_dirs:
|
||||
voices_dir = Path(voices_dir)
|
||||
|
||||
if not voices_dir.is_dir():
|
||||
continue
|
||||
|
||||
for lang_dir in voices_dir.iterdir():
|
||||
if not lang_dir.is_dir():
|
||||
continue
|
||||
|
||||
for voice_dir in lang_dir.iterdir():
|
||||
if not voice_dir.is_dir():
|
||||
continue
|
||||
|
||||
voice_lang = lang_dir.name
|
||||
voice_name = voice_dir.name
|
||||
|
||||
yield Voice(
|
||||
key=str(voice_dir.absolute()),
|
||||
name=voice_name,
|
||||
language=voice_lang,
|
||||
description="",
|
||||
)
|
||||
|
||||
def begin_utterance(self):
|
||||
self._results.clear()
|
||||
# self._current_settings = deepcopy(self.settings)
|
||||
|
||||
def speak_text(self, text: str, text_language: typing.Optional[str] = None):
|
||||
text_language = text_language or self.language
|
||||
for sentence in gruut.sentences(text, lang=text_language):
|
||||
sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
self._results.append(
|
||||
Mimic3Phonemes(
|
||||
current_settings=deepcopy(self.settings),
|
||||
phonemes=sent_phonemes,
|
||||
)
|
||||
)
|
||||
|
||||
def _speak_sentence_phonemes(
|
||||
self,
|
||||
sent_phonemes,
|
||||
text: typing.Optional[str] = None,
|
||||
settings: typing.Optional[Mimic3Settings] = None,
|
||||
) -> AudioResult:
|
||||
settings = settings or self.settings
|
||||
current_voice = self._get_or_load_voice(settings.voice or DEFAULT_VOICE)
|
||||
|
||||
config = current_voice.config
|
||||
onnx_model = current_voice.onnx_model
|
||||
phoneme_to_id = current_voice.phoneme_to_id
|
||||
phoneme_map = current_voice.phoneme_map or config.phonemes.phoneme_map
|
||||
|
||||
sent_phoneme_ids = phonemes2ids.phonemes2ids(
|
||||
word_phonemes=sent_phonemes,
|
||||
phoneme_to_id=phoneme_to_id,
|
||||
pad=config.phonemes.pad,
|
||||
bos=config.phonemes.bos,
|
||||
eos=config.phonemes.eos,
|
||||
auto_bos_eos=config.phonemes.auto_bos_eos,
|
||||
blank=config.phonemes.blank,
|
||||
blank_word=config.phonemes.blank_word,
|
||||
blank_between=config.phonemes.blank_between,
|
||||
blank_at_start=config.phonemes.blank_at_start,
|
||||
blank_at_end=config.phonemes.blank_at_end,
|
||||
simple_punctuation=config.phonemes.simple_punctuation,
|
||||
punctuation_map=config.phonemes.punctuation_map,
|
||||
separate=config.phonemes.separate,
|
||||
separate_graphemes=config.phonemes.separate_graphemes,
|
||||
separate_tones=config.phonemes.separate_tones,
|
||||
tone_before=config.phonemes.tone_before,
|
||||
phoneme_map=phoneme_map,
|
||||
fail_on_missing=False,
|
||||
)
|
||||
|
||||
if text:
|
||||
_LOGGER.debug("%s %s %s", text, sent_phonemes, sent_phoneme_ids)
|
||||
else:
|
||||
_LOGGER.debug("%s %s", sent_phonemes, sent_phoneme_ids)
|
||||
|
||||
# Create model inputs
|
||||
text_array = np.expand_dims(np.array(sent_phoneme_ids, dtype=np.int64), 0)
|
||||
text_lengths_array = np.array([text_array.shape[1]], dtype=np.int64)
|
||||
scales_array = np.array(
|
||||
[
|
||||
settings.noise_scale,
|
||||
settings.length_scale,
|
||||
settings.noise_w,
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
inputs = {
|
||||
"input": text_array,
|
||||
"input_lengths": text_lengths_array,
|
||||
"scales": scales_array,
|
||||
}
|
||||
|
||||
if config.is_multispeaker:
|
||||
speaker_id = settings.speaker_id if settings.speaker_id is not None else 0
|
||||
speaker_id_array = np.array([speaker_id], dtype=np.int64)
|
||||
inputs["sid"] = speaker_id_array
|
||||
|
||||
# Infer audio from phonemes
|
||||
start_time = time.perf_counter()
|
||||
audio = onnx_model.run(None, inputs)[0].squeeze()
|
||||
audio = audio_float_to_int16(audio)
|
||||
end_time = time.perf_counter()
|
||||
|
||||
# Compute real-time factor
|
||||
audio_duration_sec = audio.shape[-1] / config.audio.sample_rate
|
||||
infer_sec = end_time - start_time
|
||||
real_time_factor = (
|
||||
infer_sec / audio_duration_sec if audio_duration_sec > 0 else 0.0
|
||||
)
|
||||
|
||||
_LOGGER.debug("RTF: %s", real_time_factor)
|
||||
|
||||
audio_bytes = audio.tobytes()
|
||||
return AudioResult(
|
||||
sample_rate_hz=config.audio.sample_rate,
|
||||
audio_bytes=audio_bytes,
|
||||
# 16-bit mono
|
||||
sample_width_bytes=2,
|
||||
num_channels=1,
|
||||
)
|
||||
|
||||
def speak_tokens(self, tokens: typing.Iterable[BaseToken]):
|
||||
token_phonemes: PHONEMES_LIST = []
|
||||
|
||||
for token in tokens:
|
||||
if isinstance(token, Word):
|
||||
word_role = xmlescape(token.role) if token.role else ""
|
||||
word_text = xmlescape(token.text)
|
||||
|
||||
sentence = next(
|
||||
iter(
|
||||
gruut.sentences(
|
||||
f'<w role="{word_role}">{word_text}</w>', ssml=True
|
||||
)
|
||||
)
|
||||
)
|
||||
token_phonemes.extend(w.phonemes for w in sentence if w.phonemes)
|
||||
elif isinstance(token, Phonemes):
|
||||
phoneme_str = token.text.strip()
|
||||
if " " in phoneme_str:
|
||||
token_phonemes.append(phoneme_str.split())
|
||||
else:
|
||||
token_phonemes.append(list(phoneme_str))
|
||||
elif isinstance(token, SayAs):
|
||||
word_text = xmlescape(token.text)
|
||||
interpret_as = xmlescape(token.interpret_as)
|
||||
format_attr = (
|
||||
f'format="{xmlescape(token.format)}"' if token.format else ""
|
||||
)
|
||||
|
||||
sentence = next(
|
||||
iter(
|
||||
gruut.sentences(
|
||||
f'<say-as interpret-as="{interpret_as}" {format_attr}>{word_text}</say-as>',
|
||||
ssml=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
token_phonemes.extend(w.phonemes for w in sentence if w.phonemes)
|
||||
|
||||
if token_phonemes:
|
||||
self._results.append(
|
||||
Mimic3Phonemes(
|
||||
current_settings=deepcopy(self.settings), phonemes=token_phonemes
|
||||
)
|
||||
)
|
||||
|
||||
def add_break(self, time_ms: int):
|
||||
# Generate silence (16-bit mono at sample rate)
|
||||
num_bytes = int((time_ms / 1000.0) * self.settings.sample_rate * 2)
|
||||
audio_bytes = bytes(num_bytes)
|
||||
|
||||
self._results.append(
|
||||
AudioResult(
|
||||
sample_rate_hz=self.settings.sample_rate,
|
||||
audio_bytes=audio_bytes,
|
||||
# 16-bit mono
|
||||
sample_width_bytes=2,
|
||||
num_channels=1,
|
||||
)
|
||||
)
|
||||
|
||||
def set_mark(self, name: str):
|
||||
self._results.append(MarkResult(name=name))
|
||||
|
||||
def end_utterance(self) -> typing.Iterable[BaseResult]:
|
||||
last_settings = self.settings
|
||||
|
||||
sent_phonemes: PHONEMES_LIST = []
|
||||
|
||||
for result in self._results:
|
||||
if isinstance(result, Mimic3Phonemes):
|
||||
if result.current_settings != last_settings:
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(
|
||||
sent_phonemes, settings=last_settings
|
||||
)
|
||||
sent_phonemes.clear()
|
||||
|
||||
sent_phonemes.extend(result.phonemes)
|
||||
last_settings = result.current_settings
|
||||
else:
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(
|
||||
sent_phonemes, settings=last_settings
|
||||
)
|
||||
sent_phonemes.clear()
|
||||
|
||||
yield result
|
||||
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(sent_phonemes)
|
||||
|
||||
def _get_or_load_voice(self, voice_key: str) -> LoadedVoice:
|
||||
existing_voice = self.loaded_voices.get(voice_key)
|
||||
if existing_voice is not None:
|
||||
return existing_voice
|
||||
|
||||
# Look up as substring of known voice
|
||||
model_dir: typing.Optional[Path] = None
|
||||
for maybe_voice in self.get_voices():
|
||||
if maybe_voice.key.endswith(voice_key):
|
||||
model_dir = Path(maybe_voice.key)
|
||||
break
|
||||
|
||||
assert model_dir is not None
|
||||
existing_voice = self.loaded_voices.get(str(model_dir.absolute()))
|
||||
if existing_voice is not None:
|
||||
# Alias
|
||||
self.loaded_voices[voice_key] = existing_voice
|
||||
|
||||
return existing_voice
|
||||
|
||||
_LOGGER.debug("Loading voice from %s", model_dir)
|
||||
|
||||
config_path = model_dir / "config.json"
|
||||
_LOGGER.debug("Loading model config from %s", config_path)
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as config_file:
|
||||
config = TrainingConfig.load(config_file)
|
||||
|
||||
# phoneme -> id
|
||||
phoneme_ids_path = model_dir / "phonemes.txt"
|
||||
_LOGGER.debug("Loading model phonemes from %s", phoneme_ids_path)
|
||||
with open(phoneme_ids_path, "r", encoding="utf-8") as ids_file:
|
||||
phoneme_to_id = phonemes2ids.load_phoneme_ids(ids_file)
|
||||
|
||||
generator_path = model_dir / "generator.onnx"
|
||||
_LOGGER.debug("Loading model from %s", generator_path)
|
||||
|
||||
sess_options = onnxruntime.SessionOptions()
|
||||
# sess_options.enable_cpu_mem_arena = False
|
||||
# sess_options.enable_mem_pattern = False
|
||||
# sess_options.enable_mem_reuse = False
|
||||
|
||||
onnx_model = onnxruntime.InferenceSession(
|
||||
str(generator_path), sess_options=sess_options
|
||||
)
|
||||
|
||||
voice = LoadedVoice(
|
||||
config=config, onnx_model=onnx_model, phoneme_to_id=phoneme_to_id
|
||||
)
|
||||
|
||||
# valid_phonemes = []
|
||||
# for phoneme_str in self._phoneme_to_id:
|
||||
# maybe_phoneme = Phoneme(phoneme_str)
|
||||
# if any(
|
||||
# [
|
||||
# maybe_phoneme.vowel,
|
||||
# maybe_phoneme.consonant,
|
||||
# maybe_phoneme.dipthong,
|
||||
# maybe_phoneme.schwa,
|
||||
# ]
|
||||
# ):
|
||||
# valid_phonemes.append(maybe_phoneme)
|
||||
|
||||
# self._voice_phonemes = Phonemes(phonemes=valid_phonemes)
|
||||
|
||||
# phoneme -> phoneme, phoneme, ...
|
||||
phoneme_map_path = model_dir / "phoneme_map.txt"
|
||||
if phoneme_map_path.is_file():
|
||||
_LOGGER.debug("Loading phoneme map from %s", phoneme_map_path)
|
||||
with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
||||
voice.phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
||||
|
||||
_LOGGER.info("Loaded voice from %s", model_dir)
|
||||
|
||||
# Add to cache
|
||||
self.loaded_voices[voice_key] = voice
|
||||
|
||||
return voice
|
||||
|
||||
# def start(self):
|
||||
# self.stop()
|
||||
|
||||
# self._thread = threading.Thread(target=self._thread_proc, daemon=True)
|
||||
# self._thread.start()
|
||||
|
||||
# def stop(self):
|
||||
# if self._thread is not None:
|
||||
# self._request_queue.put(None)
|
||||
# self._thread.join()
|
||||
# self._thread = None
|
||||
|
||||
# # Drain queues
|
||||
# while not self._request_queue.empty():
|
||||
# self._request_queue.get()
|
||||
|
||||
# while not self._result_queue.empty():
|
||||
# self._result_queue.get()
|
||||
|
||||
# def _thread_proc(self):
|
||||
# try:
|
||||
# self._load_model()
|
||||
# self._load_text_processor()
|
||||
|
||||
# while True:
|
||||
# message = self._request_queue.get()
|
||||
# if message is None:
|
||||
# break
|
||||
|
||||
# if isinstance(message, AddLexiconMessage):
|
||||
# self._add_lexicon(message.lexicon_file)
|
||||
# elif isinstance(message, TextToSpeechMessage):
|
||||
# result = self._text_to_speech(**dataclasses.asdict(message))
|
||||
# self._result_queue.put(result)
|
||||
|
||||
# except Exception:
|
||||
# _LOGGER.exception("_thread_proc")
|
||||
|
||||
# def _load_model(self):
|
||||
# """Load model configuration and generator"""
|
||||
|
||||
# if self._config is None:
|
||||
# config_path = self.model_dir / "config.json"
|
||||
# _LOGGER.debug("Loading model config from %s", config_path)
|
||||
|
||||
# with open(config_path, "r", encoding="utf-8") as config_file:
|
||||
# self._config = TrainingConfig.load(config_file)
|
||||
|
||||
# self.lang = self.lang or self._config.text_language or "en_US"
|
||||
|
||||
# if self._phoneme_to_id is None:
|
||||
# # phoneme -> id
|
||||
# phoneme_ids_path = self.model_dir / "phonemes.txt"
|
||||
# _LOGGER.debug("Loading model phonemes from %s", phoneme_ids_path)
|
||||
# with open(phoneme_ids_path, "r", encoding="utf-8") as ids_file:
|
||||
# self._phoneme_to_id = phonemes2ids.load_phoneme_ids(ids_file)
|
||||
|
||||
# valid_phonemes = []
|
||||
# for phoneme_str in self._phoneme_to_id:
|
||||
# maybe_phoneme = Phoneme(phoneme_str)
|
||||
# if any(
|
||||
# [
|
||||
# maybe_phoneme.vowel,
|
||||
# maybe_phoneme.consonant,
|
||||
# maybe_phoneme.dipthong,
|
||||
# maybe_phoneme.schwa,
|
||||
# ]
|
||||
# ):
|
||||
# valid_phonemes.append(maybe_phoneme)
|
||||
|
||||
# self._voice_phonemes = Phonemes(phonemes=valid_phonemes)
|
||||
|
||||
# if self._phoneme_map is None:
|
||||
# # phoneme -> phoneme, phoneme, ...
|
||||
# phoneme_map_path = self.model_dir / "phoneme_map.txt"
|
||||
# if phoneme_map_path.is_file():
|
||||
# _LOGGER.debug("Loading phoneme map from %s", phoneme_map_path)
|
||||
# with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
||||
# self._phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
||||
|
||||
# if self._onnx_model is None:
|
||||
# generator_path = self.model_dir / "generator.onnx"
|
||||
# _LOGGER.debug("Loading model from %s", generator_path)
|
||||
|
||||
# sess_options = onnxruntime.SessionOptions()
|
||||
# sess_options.enable_cpu_mem_arena = False
|
||||
# sess_options.enable_mem_pattern = False
|
||||
# sess_options.enable_mem_reuse = False
|
||||
|
||||
# self._onnx_model = onnxruntime.InferenceSession(
|
||||
# str(generator_path), sess_options=sess_options
|
||||
# )
|
||||
|
||||
# def _load_text_processor(self):
|
||||
# if self._text_processor is None:
|
||||
# self._text_processor = gruut.TextProcessor(default_lang=self.lang)
|
||||
|
||||
# def add_lexicon(self, lexicon_file: typing.Iterable[str]):
|
||||
# """Load a custom pronunciation lexicon from a file.
|
||||
|
||||
# Format is:
|
||||
# <word> <role> <phoneme> <phoneme> ...
|
||||
|
||||
# Role can be things like "gruut:VB" or "gruut:NN".
|
||||
# Use "_" for the default role (any part of speech).
|
||||
# """
|
||||
# self._request_queue.put(AddLexiconMessage(lexicon_file=list(lexicon_file)))
|
||||
|
||||
# def _add_lexicon(self, lexicon_file: typing.Iterable[str]):
|
||||
# self._load_text_processor()
|
||||
# assert self._text_processor is not None
|
||||
|
||||
# # word -> role -> [phoneme, phoneme, ...]
|
||||
# lexicon: typing.Dict[str, typing.Dict[str, typing.List[str]]] = {}
|
||||
|
||||
# for line in lexicon_file:
|
||||
# line = line.strip()
|
||||
# if not line:
|
||||
# continue
|
||||
|
||||
# word, role, *phonemes = line.split()
|
||||
# if (not word) or (not phonemes):
|
||||
# _LOGGER.warning("Empty word or pronunciation in lexicon: %s", line)
|
||||
# continue
|
||||
|
||||
# if role == "_":
|
||||
# role = WordRole.DEFAULT
|
||||
|
||||
# word_roles = lexicon.get(word)
|
||||
# if word_roles is None:
|
||||
# word_roles = {}
|
||||
# lexicon[word] = word_roles
|
||||
|
||||
# word_roles[role] = phonemes
|
||||
|
||||
# if lexicon:
|
||||
|
||||
# # Wrap the "lookup_phonemes" method in the gruut text processor.
|
||||
# # Our lexicon will be consulted first.
|
||||
# settings = self._text_processor.get_settings()
|
||||
# base_lookup = settings.lookup_phonemes
|
||||
|
||||
# def lookup_phonemes(word: str, role: typing.Optional[str] = None, **kwargs):
|
||||
# word_roles = lexicon.get(word)
|
||||
|
||||
# if not word_roles:
|
||||
# # Try lower case
|
||||
# word_roles = lexicon.get(word.lower())
|
||||
|
||||
# if word_roles:
|
||||
# if role is None:
|
||||
# role = WordRole.DEFAULT
|
||||
|
||||
# phonemes = word_roles.get(role)
|
||||
|
||||
# if (phonemes is None) and (role != WordRole.DEFAULT):
|
||||
# phonemes = word_roles.get(WordRole.DEFAULT)
|
||||
|
||||
# if phonemes:
|
||||
# return phonemes
|
||||
|
||||
# if base_lookup is not None:
|
||||
# return base_lookup(word, role, **kwargs)
|
||||
|
||||
# return None
|
||||
|
||||
# settings.lookup_phonemes = typing.cast(LookupPhonemes, lookup_phonemes)
|
||||
# _LOGGER.debug("Added custom pronunciations for %s word(s)", len(lexicon))
|
||||
|
||||
# def text_to_speech(
|
||||
# self,
|
||||
# text: str,
|
||||
# speaker_id: typing.Optional[int] = None,
|
||||
# length_scale: typing.Optional[float] = None,
|
||||
# noise_scale: typing.Optional[float] = None,
|
||||
# noise_w: typing.Optional[float] = None,
|
||||
# ssml: bool = False,
|
||||
# text_language: typing.Optional[str] = None,
|
||||
# ) -> Result:
|
||||
# self._request_queue.put(
|
||||
# TextToSpeechMessage(
|
||||
# text=text,
|
||||
# speaker_id=speaker_id,
|
||||
# length_scale=length_scale,
|
||||
# noise_scale=noise_scale,
|
||||
# noise_w=noise_w,
|
||||
# ssml=ssml,
|
||||
# text_language=text_language,
|
||||
# )
|
||||
# )
|
||||
|
||||
# result = typing.cast(Result, self._result_queue.get())
|
||||
|
||||
# return result
|
||||
|
||||
# def _text_to_speech(
|
||||
# self,
|
||||
# text: str,
|
||||
# speaker_id: typing.Optional[int] = None,
|
||||
# length_scale: typing.Optional[float] = None,
|
||||
# noise_scale: typing.Optional[float] = None,
|
||||
# noise_w: typing.Optional[float] = None,
|
||||
# ssml: bool = False,
|
||||
# text_language: typing.Optional[str] = None,
|
||||
# ) -> Result:
|
||||
# """Speak text and return WAV audio as bytes"""
|
||||
# text_language = text_language or self.lang
|
||||
# assert self._text_processor is not None
|
||||
|
||||
# # Ensure model is loaded
|
||||
# assert self.lang is not None
|
||||
# assert self._config is not None
|
||||
# assert self._phoneme_to_id is not None
|
||||
# assert self._onnx_model is not None
|
||||
|
||||
# # Resolve settings
|
||||
# if speaker_id is None:
|
||||
# speaker_id = self.speaker_id or 0
|
||||
|
||||
# if length_scale is None:
|
||||
# length_scale = self.length_scale
|
||||
|
||||
# if noise_scale is None:
|
||||
# noise_scale = self.noise_scale
|
||||
|
||||
# if noise_w is None:
|
||||
# noise_w = self.noise_w
|
||||
|
||||
# # Process text into sentences
|
||||
# result = Result(text=text)
|
||||
# audio_arrays: typing.List[np.ndarray] = []
|
||||
|
||||
# graph, root = self._text_processor.process(text, lang=text_language, ssml=ssml)
|
||||
# sentences = list(self._text_processor.sentences(graph, root))
|
||||
|
||||
# for sentence in sentences:
|
||||
# result.sentence_words.append([w.text for w in sentence])
|
||||
|
||||
# if text_language == self.lang:
|
||||
# sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
# else:
|
||||
# # Convert phonemes to ids to target language
|
||||
# other_sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
# _LOGGER.debug(other_sent_phonemes)
|
||||
|
||||
# sent_phonemes = []
|
||||
# for other_word_p in other_sent_phonemes:
|
||||
# word_p = []
|
||||
# for other_p in other_word_p:
|
||||
# if IPA.is_break(other_p):
|
||||
# # Keep breaks
|
||||
# word_p.append(other_p)
|
||||
# continue
|
||||
|
||||
# original_p = other_p
|
||||
# stress = ""
|
||||
# while other_p and IPA.is_stress(other_p[0]):
|
||||
# stress = other_p[0]
|
||||
# other_p = other_p[1:]
|
||||
|
||||
# if not other_p:
|
||||
# continue
|
||||
|
||||
# if other_p in self._phoneme_to_id:
|
||||
# word_p.append(original_p)
|
||||
# continue
|
||||
|
||||
# assert self._voice_phonemes is not None
|
||||
# guessed = guess_phonemes(
|
||||
# other_p, to_phonemes=self._voice_phonemes
|
||||
# )
|
||||
# if guessed.phonemes:
|
||||
# word_p.extend([p.text for p in guessed.phonemes])
|
||||
|
||||
# if word_p:
|
||||
# sent_phonemes.append(word_p)
|
||||
|
||||
# result.sentence_phonemes.append(sent_phonemes)
|
||||
|
||||
# sent_phoneme_ids = phonemes2ids.phonemes2ids(
|
||||
# word_phonemes=sent_phonemes,
|
||||
# phoneme_to_id=self._phoneme_to_id,
|
||||
# pad=self._config.phonemes.pad,
|
||||
# bos=self._config.phonemes.bos,
|
||||
# eos=self._config.phonemes.eos,
|
||||
# auto_bos_eos=self._config.phonemes.auto_bos_eos,
|
||||
# blank=self._config.phonemes.blank,
|
||||
# blank_word=self._config.phonemes.blank_word,
|
||||
# blank_between=self._config.phonemes.blank_between,
|
||||
# blank_at_start=self._config.phonemes.blank_at_start,
|
||||
# blank_at_end=self._config.phonemes.blank_at_end,
|
||||
# simple_punctuation=self._config.phonemes.simple_punctuation,
|
||||
# punctuation_map=self._config.phonemes.punctuation_map,
|
||||
# separate=self._config.phonemes.separate,
|
||||
# separate_graphemes=self._config.phonemes.separate_graphemes,
|
||||
# separate_tones=self._config.phonemes.separate_tones,
|
||||
# tone_before=self._config.phonemes.tone_before,
|
||||
# phoneme_map=self._phoneme_map or self._config.phonemes.phoneme_map,
|
||||
# fail_on_missing=False,
|
||||
# )
|
||||
|
||||
# result.sentence_phoneme_ids.append(sent_phonemes)
|
||||
|
||||
# _LOGGER.debug("%s %s %s", sentence.text, sent_phonemes, sent_phoneme_ids)
|
||||
|
||||
# # Create model inputs
|
||||
# text_array = np.expand_dims(np.array(sent_phoneme_ids, dtype=np.int64), 0)
|
||||
# text_lengths_array = np.array([text_array.shape[1]], dtype=np.int64)
|
||||
# scales_array = np.array(
|
||||
# [noise_scale, length_scale, noise_w], dtype=np.float32
|
||||
# )
|
||||
|
||||
# inputs = {
|
||||
# "input": text_array,
|
||||
# "input_lengths": text_lengths_array,
|
||||
# "scales": scales_array,
|
||||
# }
|
||||
|
||||
# if self._config.is_multispeaker:
|
||||
# speaker_id_array = np.array([speaker_id], dtype=np.int64)
|
||||
# inputs["sid"] = speaker_id_array
|
||||
|
||||
# # Infer audio from phonemes
|
||||
# start_time = time.perf_counter()
|
||||
# audio = self._onnx_model.run(None, inputs)[0].squeeze()
|
||||
# audio = audio_float_to_int16(audio)
|
||||
# end_time = time.perf_counter()
|
||||
|
||||
# # Compute real-time factor
|
||||
# audio_duration_sec = audio.shape[-1] / self._config.audio.sample_rate
|
||||
# infer_sec = end_time - start_time
|
||||
# real_time_factor = (
|
||||
# infer_sec / audio_duration_sec if audio_duration_sec > 0 else 0.0
|
||||
# )
|
||||
|
||||
# _LOGGER.debug("RTF: %s", real_time_factor)
|
||||
|
||||
# audio_arrays.append(audio)
|
||||
|
||||
# # Write to WAV and return bytes
|
||||
# with io.BytesIO() as wav_file:
|
||||
# write_wav(
|
||||
# wav_file, self._config.audio.sample_rate, np.concatenate(audio_arrays),
|
||||
# )
|
||||
|
||||
# result.wav_bytes = wav_file.getvalue()
|
||||
|
||||
# return result
|
||||
26
mimic3-tts/mimic3_tts/utils.py
Normal file
26
mimic3-tts/mimic3_tts/utils.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2021 Mycroft AI Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import numpy as np
|
||||
|
||||
|
||||
def audio_float_to_int16(
|
||||
audio: np.ndarray, max_wav_value: float = 32767.0
|
||||
) -> np.ndarray:
|
||||
"""Normalize audio and convert to int16 range"""
|
||||
audio_norm = audio * (max_wav_value / max(0.01, np.max(np.abs(audio))))
|
||||
audio_norm = np.clip(audio_norm, -max_wav_value, max_wav_value)
|
||||
audio_norm = audio_norm.astype("int16")
|
||||
return audio_norm
|
||||
4
mimic3-tts/mypy.ini
Normal file
4
mimic3-tts/mypy.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-setuptools.*]
|
||||
ignore_missing_imports = True
|
||||
39
mimic3-tts/pylintrc
Normal file
39
mimic3-tts/pylintrc
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
format,
|
||||
abstract-class-little-used,
|
||||
abstract-method,
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
global-statement,
|
||||
import-outside-toplevel,
|
||||
inconsistent-return-statements,
|
||||
locally-disabled,
|
||||
not-context-manager,
|
||||
redefined-variable-type,
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
too-many-boolean-expressions,
|
||||
unnecessary-pass,
|
||||
unused-argument,
|
||||
broad-except,
|
||||
too-many-nested-blocks,
|
||||
invalid-name,
|
||||
unused-import,
|
||||
no-self-use,
|
||||
fixme,
|
||||
useless-super-delegation,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
import-error
|
||||
|
||||
[FORMAT]
|
||||
expected-line-ending-format=LF
|
||||
7
mimic3-tts/requirements.txt
Normal file
7
mimic3-tts/requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
dataclasses-json<1.0
|
||||
espeak-phonemizer>=1.0,<2.0
|
||||
gruut[en,de,es,nl,it,fr,sw]>=2.2.2,<3.0
|
||||
numpy<2.0
|
||||
onnxruntime>=1.6,<2.0
|
||||
phonemes2ids<2.0
|
||||
opentts_abc<1.0
|
||||
7
mimic3-tts/requirements_dev.txt
Normal file
7
mimic3-tts/requirements_dev.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
22
mimic3-tts/setup.cfg
Normal file
22
mimic3-tts/setup.cfg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[flake8]
|
||||
# To work with Black
|
||||
max-line-length = 88
|
||||
# E501: line too long
|
||||
# W503: Line break occurred before a binary operator
|
||||
# E203: Whitespace before ':'
|
||||
# D202 No blank lines allowed after function docstring
|
||||
# W504 line break after binary operator
|
||||
ignore =
|
||||
E501,
|
||||
W503,
|
||||
E203,
|
||||
D202,
|
||||
W504
|
||||
|
||||
[isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
indent = " "
|
||||
50
mimic3-tts/setup.py
Normal file
50
mimic3-tts/setup.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
from setuptools import setup
|
||||
|
||||
this_dir = Path(__file__).parent
|
||||
module_dir = this_dir / "mimic3_tts"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Load README in as long description
|
||||
long_description: str = ""
|
||||
readme_path = this_dir / "README.md"
|
||||
if readme_path.is_file():
|
||||
long_description = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
requirements = []
|
||||
requirements_path = this_dir / "requirements.txt"
|
||||
if requirements_path.is_file():
|
||||
with open(requirements_path, "r", encoding="utf-8") as requirements_file:
|
||||
requirements = requirements_file.read().splitlines()
|
||||
|
||||
version_path = module_dir / "VERSION"
|
||||
with open(version_path, "r", encoding="utf-8") as version_file:
|
||||
version = version_file.read().strip()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
setup(
|
||||
name="mimic3_tts",
|
||||
version=version,
|
||||
description="A fast, local, neural text to speech system for Mycroft",
|
||||
url="http://github.com/MycroftAI/mimic3",
|
||||
author="Michael Hansen",
|
||||
author_email="michael.hansen@mycroft.ai",
|
||||
license="Apache-2.0",
|
||||
packages=setuptools.find_packages(),
|
||||
package_data={"mimic3_tts": ["VERSION", "py.typed"]},
|
||||
install_requires=requirements,
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
)
|
||||
14
opentts-abc/.gitignore
vendored
Normal file
14
opentts-abc/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
6
opentts-abc/.isort.cfg
Normal file
6
opentts-abc/.isort.cfg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
3
opentts-abc/.projectile
Normal file
3
opentts-abc/.projectile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- /.venv/
|
||||
- /.mypy_cache/
|
||||
- /opentts_abc/.mypy_cache/
|
||||
21
opentts-abc/LICENSE
Normal file
21
opentts-abc/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Michael Hansen
|
||||
|
||||
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.
|
||||
0
opentts-abc/README.md
Normal file
0
opentts-abc/README.md
Normal file
28
opentts-abc/check.sh
Executable file
28
opentts-abc/check.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# Directory of *this* script
|
||||
this_dir="$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
# Kebab to snake case
|
||||
module_name="$(basename "${this_dir}" | sed -e 's/-/_/g')"
|
||||
src_dir="${this_dir}/${module_name}"
|
||||
|
||||
# Path to virtual environment
|
||||
: "${venv:=${this_dir}/.venv}"
|
||||
|
||||
if [ -d "${venv}" ]; then
|
||||
# Activate virtual environment if available
|
||||
source "${venv}/bin/activate"
|
||||
fi
|
||||
|
||||
# Format code
|
||||
black "${src_dir}"
|
||||
isort "${src_dir}"
|
||||
|
||||
# Check
|
||||
flake8 "${src_dir}"
|
||||
pylint "${src_dir}"
|
||||
mypy "${src_dir}"
|
||||
|
||||
echo 'OK'
|
||||
34
opentts-abc/install.sh
Executable file
34
opentts-abc/install.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/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"
|
||||
4
opentts-abc/mypy.ini
Normal file
4
opentts-abc/mypy.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-setuptools.*]
|
||||
ignore_missing_imports = True
|
||||
1
opentts-abc/opentts_abc/VERSION
Normal file
1
opentts-abc/opentts_abc/VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.1.0
|
||||
162
opentts-abc/opentts_abc/__init__.py
Normal file
162
opentts-abc/opentts_abc/__init__.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Base classes for Open Text to Speech systems"""
|
||||
import dataclasses
|
||||
import io
|
||||
import typing
|
||||
import wave
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from contextlib import AbstractContextManager
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
voice: typing.Optional[str] = None
|
||||
language: typing.Optional[str] = None
|
||||
volume: typing.Optional[float] = None
|
||||
rate: typing.Optional[float] = None
|
||||
pitch: typing.Optional[float] = None
|
||||
active_lexicons: typing.Optional[typing.Sequence[str]] = None
|
||||
other_settings: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseToken(metaclass=ABCMeta):
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Word(BaseToken):
|
||||
role: typing.Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Phonemes(BaseToken):
|
||||
alphabet: typing.Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SayAs(BaseToken):
|
||||
interpret_as: str
|
||||
format: typing.Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BaseResultDefaults:
|
||||
tag: typing.Optional[typing.Any] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseResult(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class _AudioResultBase:
|
||||
sample_rate_hz: int
|
||||
sample_width_bytes: int
|
||||
num_channels: int
|
||||
audio_bytes: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioResult(BaseResult, _BaseResultDefaults, _AudioResultBase):
|
||||
def to_wav_bytes(self) -> bytes:
|
||||
with io.BytesIO() as wav_io:
|
||||
wav_file: wave.Wave_write = wave.open(wav_io, "wb")
|
||||
with wav_file:
|
||||
wav_file.setframerate(self.sample_rate_hz)
|
||||
wav_file.setsampwidth(self.sample_width_bytes)
|
||||
wav_file.setnchannels(self.num_channels)
|
||||
wav_file.writeframes(self.audio_bytes)
|
||||
|
||||
return wav_io.getvalue()
|
||||
|
||||
|
||||
@dataclass
|
||||
class _MarkResultBase:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarkResult(BaseResult, _BaseResultDefaults, _MarkResultBase):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Voice:
|
||||
key: str
|
||||
name: str
|
||||
language: str
|
||||
description: str
|
||||
properties: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
||||
|
||||
|
||||
# @dataclass
|
||||
# class LexiconEntry:
|
||||
# word: str
|
||||
# pronunciation: str
|
||||
# role: typing.Optional[str] = None
|
||||
|
||||
|
||||
# @dataclass
|
||||
# class Lexicon:
|
||||
# name: str
|
||||
# entries: typing.Mapping[str, typing.Sequence[LexiconEntry]]
|
||||
|
||||
|
||||
class TextToSpeechSystem(AbstractContextManager, metaclass=ABCMeta):
|
||||
"""Abstract base class for open text to speech systems"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def voice(self) -> str:
|
||||
pass
|
||||
|
||||
@voice.setter
|
||||
def voice(self, new_voice: str):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def language(self) -> str:
|
||||
pass
|
||||
|
||||
@language.setter
|
||||
def language(self, new_language: str):
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.shutdown()
|
||||
|
||||
@abstractmethod
|
||||
def get_voices(self) -> typing.Iterable[Voice]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def begin_utterance(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def speak_text(self, text: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def speak_tokens(self, tokens: typing.Iterable[BaseToken]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_break(self, time_ms: int):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_mark(self, name: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def end_utterance(self) -> typing.Iterable[BaseResult]:
|
||||
pass
|
||||
0
opentts-abc/opentts_abc/py.typed
Normal file
0
opentts-abc/opentts_abc/py.typed
Normal file
449
opentts-abc/opentts_abc/ssml.py
Normal file
449
opentts-abc/opentts_abc/ssml.py
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
#!/usr/bin/env python3
|
||||
import enum
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
import xml.etree.ElementTree as etree
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opentts_abc import (
|
||||
BaseResult,
|
||||
Phonemes,
|
||||
SayAs,
|
||||
Settings,
|
||||
TextToSpeechSystem,
|
||||
Word,
|
||||
)
|
||||
|
||||
LOG = logging.getLogger("opentts_abc.ssml")
|
||||
NO_NAMESPACE_PATTERN = re.compile(r"^{[^}]+}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class EndElement:
|
||||
"""Wrapper for end of an XML element (used in TextProcessor)"""
|
||||
|
||||
element: etree.Element
|
||||
|
||||
|
||||
class ParsingState(int, enum.Enum):
|
||||
"""Current state of SSML parsing"""
|
||||
|
||||
DEFAULT = enum.auto()
|
||||
|
||||
IN_SENTENCE = enum.auto()
|
||||
"""Inside <s>"""
|
||||
|
||||
IN_WORD = enum.auto()
|
||||
"""Inside <w> or <token>"""
|
||||
|
||||
IN_SUB = enum.auto()
|
||||
"""Inside <sub>"""
|
||||
|
||||
IN_PHONEME = enum.auto()
|
||||
"""Inside <phoneme>"""
|
||||
|
||||
IN_LEXICON = enum.auto()
|
||||
"""Inside <lexicon>"""
|
||||
|
||||
IN_LEXICON_GRAPHEME = enum.auto()
|
||||
"""Inside <lexicon><grapheme>..."""
|
||||
|
||||
IN_LEXICON_PHONEME = enum.auto()
|
||||
"""Inside <lexicon><phoneme>..."""
|
||||
|
||||
IN_METADATA = enum.auto()
|
||||
"""Inside <metadata>"""
|
||||
|
||||
IN_SAY_AS = enum.auto()
|
||||
"""Inside <say-as>"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SSMLSpeaker:
|
||||
def __init__(self, tts: TextToSpeechSystem):
|
||||
self.state_stack: typing.List[ParsingState] = [ParsingState.DEFAULT]
|
||||
self.element_stack: typing.List[etree.Element] = []
|
||||
self.voice_stack: typing.List[str] = []
|
||||
self.lang_stack: typing.List[str] = []
|
||||
self.interpret_as: typing.Optional[str] = None
|
||||
self.say_as_format: typing.Optional[str] = None
|
||||
self.tts = tts
|
||||
|
||||
def speak(
|
||||
self, ssml: typing.Union[str, etree.Element]
|
||||
) -> typing.Iterable[BaseResult]:
|
||||
|
||||
if isinstance(ssml, etree.Element):
|
||||
root_element = ssml
|
||||
else:
|
||||
root_element = etree.fromstring(ssml)
|
||||
|
||||
# Process sub-elements and text chunks
|
||||
for elem_or_text in text_and_elements(root_element):
|
||||
if isinstance(elem_or_text, str):
|
||||
if self.state in {ParsingState.IN_METADATA}:
|
||||
# Skip metadata text
|
||||
continue
|
||||
|
||||
# Text chunk
|
||||
text = typing.cast(str, elem_or_text)
|
||||
self.handle_text(text)
|
||||
elif isinstance(elem_or_text, EndElement):
|
||||
# End of an element (e.g., </w>)
|
||||
end_elem = typing.cast(EndElement, elem_or_text)
|
||||
end_tag = tag_no_namespace(end_elem.element.tag)
|
||||
|
||||
if end_tag == "s":
|
||||
yield from self.handle_end_sentence()
|
||||
elif end_tag in {"w", "token"}:
|
||||
self.handle_end_word()
|
||||
elif end_tag in {"phoneme"}:
|
||||
self.handle_end_phoneme()
|
||||
elif end_tag == "voice":
|
||||
self.handle_end_voice()
|
||||
elif end_tag == "say-as":
|
||||
self.handle_end_say_as()
|
||||
elif end_tag in {"sub"}:
|
||||
# Handled in handle_text
|
||||
pass
|
||||
elif end_tag in {"metadata", "meta"}:
|
||||
self.handle_end_metadata()
|
||||
else:
|
||||
LOG.debug("Ignoring end tag: %s", end_tag)
|
||||
else:
|
||||
if self.state in {ParsingState.IN_METADATA}:
|
||||
# Skip metadata text
|
||||
continue
|
||||
|
||||
# Start of an element (e.g., <p>)
|
||||
elem, elem_metadata = elem_or_text
|
||||
elem = typing.cast(etree.Element, elem)
|
||||
|
||||
# Optional metadata for the element
|
||||
elem_metadata = typing.cast(
|
||||
typing.Optional[typing.Dict[str, typing.Any]], elem_metadata
|
||||
)
|
||||
|
||||
elem_tag = tag_no_namespace(elem.tag)
|
||||
|
||||
if elem_tag == "s":
|
||||
self.handle_begin_sentence()
|
||||
elif elem_tag in {"w", "token"}:
|
||||
self.handle_begin_word(elem)
|
||||
elif elem_tag == "sub":
|
||||
self.handle_begin_sub(elem)
|
||||
elif elem_tag == "phoneme":
|
||||
self.handle_begin_phoneme(elem)
|
||||
elif elem_tag == "break":
|
||||
self.handle_break(elem)
|
||||
elif elem_tag == "mark":
|
||||
self.handle_mark(elem)
|
||||
elif elem_tag == "voice":
|
||||
self.handle_begin_voice(elem)
|
||||
elif elem_tag == "say-as":
|
||||
self.handle_begin_say_as(elem)
|
||||
elif elem_tag in {"metadata", "meta"}:
|
||||
self.handle_begin_metadata()
|
||||
else:
|
||||
LOG.debug("Ignoring start tag: %s", elem_tag)
|
||||
|
||||
assert self.state in {
|
||||
ParsingState.IN_SENTENCE,
|
||||
ParsingState.DEFAULT,
|
||||
}, self.state
|
||||
if self.state in {ParsingState.IN_SENTENCE}:
|
||||
yield from self.handle_end_sentence()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def handle_text(self, text: str):
|
||||
assert self.state in {
|
||||
ParsingState.DEFAULT,
|
||||
ParsingState.IN_SENTENCE,
|
||||
ParsingState.IN_WORD,
|
||||
ParsingState.IN_SUB,
|
||||
ParsingState.IN_PHONEME,
|
||||
ParsingState.IN_SAY_AS,
|
||||
}, self.state
|
||||
|
||||
if self.state == ParsingState.IN_PHONEME:
|
||||
# Phonemes were emitted in handle_begin_phoneme
|
||||
return
|
||||
|
||||
if self.state == ParsingState.IN_SUB:
|
||||
# Substitute text
|
||||
assert self.element is not None
|
||||
text = attrib_no_namespace(self.element, "alias", "")
|
||||
LOG.debug("alias text: %s", text)
|
||||
|
||||
# Terminate <sub> early
|
||||
self.handle_end_sub()
|
||||
|
||||
if self.state == ParsingState.DEFAULT:
|
||||
self.handle_begin_sentence()
|
||||
|
||||
LOG.debug("text: %s", text)
|
||||
|
||||
if self.state == ParsingState.IN_WORD:
|
||||
self.handle_word(text, self.element)
|
||||
elif self.state == ParsingState.IN_SAY_AS:
|
||||
assert self.interpret_as is not None
|
||||
self.tts.speak_tokens(
|
||||
[
|
||||
SayAs(
|
||||
text=text,
|
||||
interpret_as=self.interpret_as,
|
||||
format=self.say_as_format,
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
self.tts.speak_text(text)
|
||||
|
||||
def handle_begin_word(self, elem: etree.Element):
|
||||
LOG.debug("begin word")
|
||||
self.push_element(elem)
|
||||
self.push_state(ParsingState.IN_WORD)
|
||||
|
||||
def handle_word(self, text: str, elem: typing.Optional[etree.Element] = None):
|
||||
assert self.state in {ParsingState.IN_WORD}, self.state
|
||||
|
||||
role: typing.Optional[str] = None
|
||||
if elem is not None:
|
||||
role = attrib_no_namespace(elem, "role")
|
||||
|
||||
self.tts.speak_tokens([Word(text, role=role)])
|
||||
|
||||
def handle_end_word(self):
|
||||
LOG.debug("end word")
|
||||
assert self.state in {ParsingState.IN_WORD}, self.state
|
||||
self.pop_state()
|
||||
self.pop_element()
|
||||
|
||||
def handle_begin_sub(self, elem: etree.Element):
|
||||
LOG.debug("begin sub")
|
||||
self.push_element(elem)
|
||||
self.push_state(ParsingState.IN_SUB)
|
||||
|
||||
def handle_end_sub(self):
|
||||
LOG.debug("end sub")
|
||||
assert self.state in {ParsingState.IN_SUB}, self.state
|
||||
self.pop_state()
|
||||
self.pop_element()
|
||||
|
||||
def handle_begin_phoneme(self, elem: etree.Element):
|
||||
LOG.debug("begin phoneme")
|
||||
|
||||
if self.state == ParsingState.DEFAULT:
|
||||
self.handle_begin_sentence()
|
||||
|
||||
phonemes = attrib_no_namespace(elem, "ph", "")
|
||||
alphabet = attrib_no_namespace(elem, "alphabet", "")
|
||||
|
||||
LOG.debug("phonemes: %s", phonemes)
|
||||
|
||||
self.tts.speak_tokens([Phonemes(text=phonemes, alphabet=alphabet)])
|
||||
|
||||
self.push_element(elem)
|
||||
self.push_state(ParsingState.IN_PHONEME)
|
||||
|
||||
def handle_end_phoneme(self):
|
||||
LOG.debug("end phoneme")
|
||||
assert self.state in {ParsingState.IN_PHONEME}, self.state
|
||||
self.pop_state()
|
||||
self.pop_element()
|
||||
|
||||
def handle_begin_metadata(self):
|
||||
LOG.debug("begin metadata")
|
||||
self.push_state(ParsingState.IN_METADATA)
|
||||
|
||||
def handle_end_metadata(self):
|
||||
LOG.debug("end metadata")
|
||||
assert self.state in {ParsingState.IN_METADATA}, self.state
|
||||
self.pop_state()
|
||||
|
||||
def handle_begin_sentence(self):
|
||||
LOG.debug("begin sentence")
|
||||
assert self.state in {ParsingState.DEFAULT}, self.state
|
||||
self.push_state(ParsingState.IN_SENTENCE)
|
||||
self.tts.begin_utterance()
|
||||
|
||||
def handle_end_sentence(self) -> typing.Iterable[BaseResult]:
|
||||
LOG.debug("end sentence")
|
||||
assert self.state in {ParsingState.IN_SENTENCE}, self.state
|
||||
self.pop_state()
|
||||
|
||||
yield from self.tts.end_utterance()
|
||||
|
||||
def handle_begin_voice(self, elem: etree.Element):
|
||||
LOG.debug("begin voice")
|
||||
voice_name = attrib_no_namespace(elem, "name")
|
||||
|
||||
LOG.debug("voice: %s", voice_name)
|
||||
self.push_voice(voice_name)
|
||||
|
||||
# Set new voice
|
||||
self.tts.voice = voice_name
|
||||
|
||||
def handle_end_voice(self):
|
||||
LOG.debug("end voice")
|
||||
voice_name = self.pop_voice()
|
||||
|
||||
# Restore voice
|
||||
self.tts.voice = voice_name
|
||||
|
||||
def handle_break(self, elem: etree.Element):
|
||||
time_str = attrib_no_namespace(elem, "time", "").strip()
|
||||
time_ms: int = 0
|
||||
|
||||
if time_str.endswith("ms"):
|
||||
time_ms = int(time_str[:-2])
|
||||
elif time_str.endswith("s"):
|
||||
time_ms = int(float(time_str[:-1]) * 1000)
|
||||
|
||||
if time_ms > 0:
|
||||
LOG.debug("Break: %s ms", time_ms)
|
||||
self.tts.add_break(time_ms)
|
||||
|
||||
def handle_mark(self, elem: etree.Element):
|
||||
name = attrib_no_namespace(elem, "name", "")
|
||||
|
||||
LOG.debug("Mark: %s", name)
|
||||
self.tts.set_mark(name)
|
||||
|
||||
def handle_begin_say_as(self, elem: etree.Element):
|
||||
LOG.debug("begin say-as")
|
||||
self.interpret_as = attrib_no_namespace(elem, "interpret-as", "")
|
||||
self.say_as_format = attrib_no_namespace(elem, "format", "")
|
||||
|
||||
LOG.debug("Say as %s, format=%s", self.interpret_as, self.say_as_format)
|
||||
self.push_state(ParsingState.IN_SAY_AS)
|
||||
|
||||
def handle_end_say_as(self):
|
||||
LOG.debug("end say-as")
|
||||
assert self.state in {ParsingState.IN_SAY_AS}
|
||||
self.interpret_as = None
|
||||
self.say_as_format = None
|
||||
self.pop_state()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def state(self) -> ParsingState:
|
||||
if self.state_stack:
|
||||
return self.state_stack[-1]
|
||||
|
||||
return ParsingState.DEFAULT
|
||||
|
||||
def push_state(self, new_state: ParsingState):
|
||||
self.state_stack.append(new_state)
|
||||
|
||||
def pop_state(self) -> ParsingState:
|
||||
if self.state_stack:
|
||||
return self.state_stack.pop()
|
||||
|
||||
return ParsingState.DEFAULT
|
||||
|
||||
@property
|
||||
def element(self) -> typing.Optional[etree.Element]:
|
||||
if self.element_stack:
|
||||
return self.element_stack[-1]
|
||||
|
||||
return None
|
||||
|
||||
def push_element(self, new_element: etree.Element):
|
||||
self.element_stack.append(new_element)
|
||||
|
||||
def pop_element(self) -> typing.Optional[etree.Element]:
|
||||
if self.element_stack:
|
||||
return self.element_stack.pop()
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def lang(self) -> typing.Optional[str]:
|
||||
if self.lang_stack:
|
||||
return self.lang_stack[-1]
|
||||
|
||||
return self.tts.language
|
||||
|
||||
def push_lang(self, new_lang: str):
|
||||
self.lang_stack.append(new_lang)
|
||||
|
||||
def pop_lang(self) -> typing.Optional[str]:
|
||||
if self.lang_stack:
|
||||
return self.lang_stack.pop()
|
||||
|
||||
return self.tts.language
|
||||
|
||||
@property
|
||||
def voice(self) -> typing.Optional[str]:
|
||||
if self.voice_stack:
|
||||
return self.voice_stack[-1]
|
||||
|
||||
return self.tts.voice
|
||||
|
||||
def push_voice(self, new_voice: str):
|
||||
self.voice_stack.append(new_voice)
|
||||
|
||||
def pop_voice(self) -> typing.Optional[str]:
|
||||
if self.voice_stack:
|
||||
return self.voice_stack.pop()
|
||||
|
||||
return self.tts.voice
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def tag_no_namespace(tag: str) -> str:
|
||||
"""Remove namespace from XML tag"""
|
||||
return NO_NAMESPACE_PATTERN.sub("", tag)
|
||||
|
||||
|
||||
def attrib_no_namespace(
|
||||
element: etree.Element, name: str, default: typing.Any = None
|
||||
) -> typing.Any:
|
||||
"""Search for an attribute by key without namespaces"""
|
||||
for key, value in element.attrib.items():
|
||||
key_no_ns = NO_NAMESPACE_PATTERN.sub("", key)
|
||||
if key_no_ns == name:
|
||||
return value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def text_and_elements(element, is_last=False):
|
||||
"""Yields element, text, sub-elements, end element, and tail"""
|
||||
element_metadata = None
|
||||
|
||||
if is_last:
|
||||
# True if this is the last child element of a parent.
|
||||
# Used to preserve whitespace.
|
||||
element_metadata = {"is_last": True}
|
||||
|
||||
yield element, element_metadata
|
||||
|
||||
# Text before any tags (or end tag)
|
||||
text = element.text if element.text is not None else ""
|
||||
if text.strip():
|
||||
yield text
|
||||
|
||||
children = list(element)
|
||||
last_child_idx = len(children) - 1
|
||||
|
||||
for child_idx, child in enumerate(children):
|
||||
# Sub-elements
|
||||
is_last = child_idx == last_child_idx
|
||||
yield from text_and_elements(child, is_last=is_last)
|
||||
|
||||
# End of current element
|
||||
yield EndElement(element)
|
||||
|
||||
# Text after the current tag
|
||||
tail = element.tail if element.tail is not None else ""
|
||||
if tail.strip():
|
||||
yield tail
|
||||
7
opentts-abc/requirements_dev.txt
Normal file
7
opentts-abc/requirements_dev.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
22
opentts-abc/setup.cfg
Normal file
22
opentts-abc/setup.cfg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[flake8]
|
||||
# To work with Black
|
||||
max-line-length = 88
|
||||
# E501: line too long
|
||||
# W503: Line break occurred before a binary operator
|
||||
# E203: Whitespace before ':'
|
||||
# D202 No blank lines allowed after function docstring
|
||||
# W504 line break after binary operator
|
||||
ignore =
|
||||
E501,
|
||||
W503,
|
||||
E203,
|
||||
D202,
|
||||
W504
|
||||
|
||||
[isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
indent = " "
|
||||
50
opentts-abc/setup.py
Normal file
50
opentts-abc/setup.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
from setuptools import setup
|
||||
|
||||
this_dir = Path(__file__).parent
|
||||
module_dir = this_dir / "opentts_abc"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Load README in as long description
|
||||
long_description: str = ""
|
||||
readme_path = this_dir / "README.md"
|
||||
if readme_path.is_file():
|
||||
long_description = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
requirements = []
|
||||
requirements_path = this_dir / "requirements.txt"
|
||||
if requirements_path.is_file():
|
||||
with open(requirements_path, "r", encoding="utf-8") as requirements_file:
|
||||
requirements = requirements_file.read().splitlines()
|
||||
|
||||
version_path = module_dir / "VERSION"
|
||||
with open(version_path, "r", encoding="utf-8") as version_file:
|
||||
version = version_file.read().strip()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
setup(
|
||||
name="opentts_abc",
|
||||
version=version,
|
||||
description="Abstract base classes for Open Text to Speech system",
|
||||
url="http://github.com/synesthesiam/opentts-abc",
|
||||
author="Michael Hansen",
|
||||
author_email="mike@rhasspy.org",
|
||||
license="MIT",
|
||||
packages=setuptools.find_packages(),
|
||||
package_data={"opentts_abc": ["VERSION", "py.typed"]},
|
||||
install_requires=requirements,
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue