From 50d4c2a6c9ae57e9e8e266b8c6e4821ad40741ec Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 21 Mar 2022 15:41:48 -0400 Subject: [PATCH] Add voice selection to CLI --- mimic3-cli/mimic3_cli/__main__.py | 106 +++++++++++++++--------------- mimic3-cli/mypy.ini | 3 + mimic3-cli/requirements.txt | 1 + mimic3-cli/run.sh | 20 ++++++ 4 files changed, 77 insertions(+), 53 deletions(-) create mode 100755 mimic3-cli/run.sh diff --git a/mimic3-cli/mimic3_cli/__main__.py b/mimic3-cli/mimic3_cli/__main__.py index 032282f..fa6d44b 100644 --- a/mimic3-cli/mimic3_cli/__main__.py +++ b/mimic3-cli/mimic3_cli/__main__.py @@ -9,6 +9,7 @@ import string import subprocess import sys import threading +import tempfile import time import typing import urllib.parse @@ -29,6 +30,7 @@ _PACKAGE = "mimic3_cli" _LOGGER = logging.getLogger(_PACKAGE) + # ----------------------------------------------------------------------------- @@ -44,7 +46,7 @@ class CommandLineInterfaceState: sample_width_bytes: int = 2 num_channels: int = 1 - raw_queue: typing.Optional["Queue[bytes]"] = None + raw_queue: typing.Optional["Queue[typing.Optional[bytes]]"] = None raw_stream_thread: typing.Optional[threading.Thread] = None @@ -202,6 +204,9 @@ def initialize_tts(state: CommandLineInterfaceState): # TODO: voice/speaker state.tts = Mimic3TextToSpeechSystem(Mimic3Settings()) + if state.args.voice: + state.tts.voice = state.args.voice + # max_thread_workers: typing.Optional[int] = None # if args.max_thread_workers is not None: @@ -241,20 +246,6 @@ def initialize_tts(state: CommandLineInterfaceState): ) 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 @@ -284,8 +275,7 @@ def process_line(line_id: str, line: str, state: CommandLineInterfaceState): if not wav_bytes: wav_bytes = result.to_wav_bytes() - # play_audio(wav_bytes) - pass + play_wav_bytes(wav_bytes) if args.output_dir: if not wav_bytes: @@ -314,12 +304,12 @@ def process_line(line_id: str, line: str, state: CommandLineInterfaceState): 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 + 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): @@ -417,14 +407,26 @@ def process_lines(state: CommandLineInterfaceState): 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) + if sys.stdout.isatty() and (not state.args.stdout): + with io.BytesIO() as wav_io: + wav_file_play: wave.Wave_write = wave.open(wav_io, "wb") + with wav_file_play: + wav_file_play.setframerate(state.sample_rate_hz) + wav_file_play.setsampwidth(state.sample_width_bytes) + wav_file_play.setnchannels(state.num_channels) + wav_file_play.writeframes(state.all_audio) - sys.stdout.buffer.flush() + play_wav_bytes(wav_io.getvalue()) + else: + # Write output directly to stdout + wav_file_write: wave.Wave_write = wave.open(sys.stdout.buffer, "wb") + with wav_file_write: + wav_file_write.setframerate(state.sample_rate_hz) + wav_file_write.setsampwidth(state.sample_width_bytes) + wav_file_write.setnchannels(state.num_channels) + wav_file_write.writeframes(state.all_audio) + + sys.stdout.buffer.flush() def shutdown_tts(state: CommandLineInterfaceState): @@ -433,6 +435,17 @@ def shutdown_tts(state: CommandLineInterfaceState): state.tts = None +def play_wav_bytes(wav_bytes: bytes): + from playsound import playsound + + with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as wav_file: + wav_file.write(wav_bytes) + wav_file.seek(0) + + _LOGGER.debug("Playing WAV file: %s", wav_file.name) + playsound(wav_file.name) + + # ----------------------------------------------------------------------------- @@ -451,12 +464,11 @@ def get_args(): default=StdinFormat.AUTO, help="Format of stdin text (default: auto)", ) - # parser.add_argument( - # "--voice", - # "-v", - # default="en-us", - # help="Name of voice (expected in /)", - # ) + parser.add_argument( + "--voice", + "-v", + help="Name of voice (expected in /)", + ) # parser.add_argument( # "--voices-dir", # help="Directory with voices (format is /)", @@ -512,11 +524,6 @@ def get_args(): 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", @@ -533,12 +540,6 @@ def get_args(): 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"], @@ -546,12 +547,11 @@ def get_args(): # 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( + "--stdout", + action="store_true", + help="Force audio output to stdout even if a tty is detected", + ) 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( diff --git a/mimic3-cli/mypy.ini b/mimic3-cli/mypy.ini index 7d81e3f..4b2414c 100644 --- a/mimic3-cli/mypy.ini +++ b/mimic3-cli/mypy.ini @@ -1,4 +1,7 @@ [mypy] +[mypy-playsound.*] +ignore_missing_imports = True + [mypy-setuptools.*] ignore_missing_imports = True diff --git a/mimic3-cli/requirements.txt b/mimic3-cli/requirements.txt index e214474..4599625 100644 --- a/mimic3-cli/requirements.txt +++ b/mimic3-cli/requirements.txt @@ -1 +1,2 @@ mimic3-tts<1.0 +playsound~=1.3.0 diff --git a/mimic3-cli/run.sh b/mimic3-cli/run.sh new file mode 100755 index 0000000..9f59531 --- /dev/null +++ b/mimic3-cli/run.sh @@ -0,0 +1,20 @@ +#!/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 + +export PYTHONPATH="${this_dir}" +python3 -m "${module_name}" "$@"