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