Add the worker for the text to speech

This commit is contained in:
Sameer Rahmani 2023-05-20 00:15:50 +01:00
parent c2fe5e226c
commit a24ae9cc8d
Signed by: lxsameer
GPG Key ID: B0A4AF28AB9FD90B
4 changed files with 449 additions and 92 deletions

View File

@ -13,94 +13,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import asyncio
from pathlib import Path
import msgpack
# pylint: disable=redefined-outer-name, unused-argument
import numpy as np
import simpleaudio as sa
from TTS.utils.manage import ModelManager
from TTS.utils.synthesizer import Synthesizer
from meissa import utils
def to_wav_data(wav):
wav_norm = np.array(wav) * (32767 / max(0.01, np.max(np.abs(wav))))
return wav_norm.astype(np.int16)
def play(wav):
try:
play = sa.play_buffer(to_wav_data(wav), 1, 2, 22050)
# Wait for audio playback to finish before exiting
play.wait_done()
finally:
play.stop()
def synth(ctx):
path = Path(__file__).parent / "speaker.models.json"
manager = ModelManager(path)
language_ids_file_path = None
vocoder_path = None
vocoder_config_path = None
encoder_path = None
encoder_config_path = None
model_path, config_path, model_item = manager.download_model(
ctx.get("MODEL_NAME", "tts_models/en/ljspeech/tacotron2-DDC")
)
vocoder_name = model_item["default_vocoder"]
vocoder_path, vocoder_config_path, _ = manager.download_model(vocoder_name)
speaker_idx = ctx.get("SPEAKER_IDX")
# load models
synthesizer = Synthesizer(
model_path,
config_path,
None,
language_ids_file_path,
vocoder_path,
vocoder_config_path,
encoder_path,
encoder_config_path,
False,
)
async def tcp_handler(reader, writer):
while True:
# Read till EOF
data = await reader.read(4)
msg_len = int.from_bytes(data, "big")
if msg_len != 0:
data = await reader.read(msg_len)
message = data.decode()
print(f"Received {message!r}")
if message == "//close":
break
wav = synthesizer.tts(message, speaker_idx, "None", None)
synthesizer.save_wav(wav, "/tmp/blah.wav")
play(wav)
writer.write(b"Ok")
await writer.drain()
writer.close()
return tcp_handler
from meissa import utils, worker
class Server:
@ -110,27 +27,65 @@ class Server:
def __init__(self, ctx):
self.queue = asyncio.Queue()
self.current_job = None
self.running = True
self.stop_event = asyncio.Event()
self.worker_stop = asyncio.Event()
self.ctx = ctx
self.worker = None
def create_worker(self):
self.worker = asyncio.create_task(
worker.worker(
self.ctx,
self.queue,
self.worker_stop,
self.stop_event,
)
)
def stop_worker(self):
self.worker_stop.set()
self.stop_event.set()
self.empty_queue()
def empty_queue(self):
while not self.queue.empty():
job = self.queue.get_nowait()
job.task_done()
def verify_speech_payload(self, payload):
if not payload.get("text"):
return self.err(
"'text' field is mandatory for 'enqueue', 'clear_n_enqueue'"
)
return None
async def command_stop(self):
self.running = False
utils.info("Stopping all jobs")
self.stop_worker()
self.create_worker()
return self.ok()
async def command_stop_and_play(self, job):
self.running = False
async def command_clear_n_enqueue(self, job):
err = self.verify_speech_payload(job)
if err:
return err
self.stop_worker()
self.create_worker()
utils.info(f"Stopping all jobs and starting: {job}")
self.current_job = job
self.queue.put_nowait(job)
return self.ok()
async def command_enqueue(self, job):
err = self.verify_speech_payload(job)
if err:
return err
self.queue.put_nowait(job)
utils.info(f"Enqueued job: {job}")
return self.ok()
async def command_status(self, payload):
async def command_status(self, _):
return self.ok({"queue": self.queue.qsize()})
def err(self, msg):
@ -158,6 +113,8 @@ class Server:
return self.err(f"No command '{command}'!")
async def handle_client(self, reader, writer):
self.create_worker()
while True:
data = await reader.read(1024)
if not data:

103
meissa/worker.py Normal file
View File

@ -0,0 +1,103 @@
# Meissa - A trainable and simple text to speech server
#
# Copyright (c) 2023 Sameer Rahmani <lxsameer@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
from pathlib import Path
# pylint: disable=redefined-outer-name, unused-argument
import numpy as np
import simpleaudio as sa
from TTS.utils.manage import ModelManager
from TTS.utils.synthesizer import Synthesizer
from meissa import utils
def to_wav_data(wav):
wav_norm = np.array(wav) * (32767 / max(0.01, np.max(np.abs(wav))))
return wav_norm.astype(np.int16)
def play(wav):
return sa.play_buffer(to_wav_data(wav), 1, 2, 22050)
def create_synth(ctx):
path = Path(__file__).parent / "speaker.models.json"
manager = ModelManager(path)
language_ids_file_path = None
vocoder_path = None
vocoder_config_path = None
encoder_path = None
encoder_config_path = None
model_path, config_path, model_item = manager.download_model(
utils.config(ctx).get("model_name", "tts_models/en/ljspeech/tacotron2-DDC")
)
vocoder_name = model_item["default_vocoder"]
vocoder_path, vocoder_config_path, _ = manager.download_model(vocoder_name)
# load models
return Synthesizer(
model_path,
config_path,
None,
language_ids_file_path,
vocoder_path,
vocoder_config_path,
encoder_path,
encoder_config_path,
False,
)
async def worker(ctx, job_queue, worker_stop, stop_event):
utils.info("Spawning a worker...")
synthesizer = create_synth(ctx)
speaker_idx = utils.config(ctx).get("SPEAKER_IDX")
while not worker_stop.is_set():
try:
job = await job_queue.get()
utils.info(f"Running job: {job}")
txt = job["text"]
speaker = job.get("speaker", speaker_idx)
wav = synthesizer.tts(txt, speaker, "None", None)
# uncomment for debugging
synthesizer.save_wav(wav, "/tmp/blah.wav")
utils.info("Playing...")
try:
# wait until the audio finish playing or the stop
# event is set by the sever. E.g. via a command
# from client
player = play(wav)
while not stop_event.is_set() and player.is_playing:
await asyncio.sleep(0.05)
finally:
player.stop()
stop_event.clear()
job_queue.task_done()
except asyncio.QueueEmpty:
await asyncio.sleep(0.1)
utils.info("Worker stopped")

298
poetry.lock generated
View File

@ -148,6 +148,18 @@ files = [
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
[[package]]
name = "appnope"
version = "0.1.3"
description = "Disable App Nap on macOS >= 10.9"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
{file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
]
[[package]]
name = "astroid"
version = "2.15.3"
@ -165,6 +177,24 @@ lazy-object-proxy = ">=1.4.0"
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""}
[[package]]
name = "asttokens"
version = "2.2.1"
description = "Annotate AST trees with source code positions"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"},
{file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"},
]
[package.dependencies]
six = "*"
[package.extras]
test = ["astroid", "pytest"]
[[package]]
name = "async-timeout"
version = "4.0.2"
@ -219,6 +249,18 @@ files = [
{file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
]
[[package]]
name = "backcall"
version = "0.2.0"
description = "Specifications for callback functions passed in to an API"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
[[package]]
name = "bangla"
version = "0.0.2"
@ -714,6 +756,21 @@ files = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
[[package]]
name = "executing"
version = "1.2.0"
description = "Get the currently executing AST node of a frame, and other information"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"},
{file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"},
]
[package.extras]
tests = ["asttokens", "littleutils", "pytest", "rich"]
[[package]]
name = "filelock"
version = "3.12.0"
@ -1032,6 +1089,62 @@ files = [
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
name = "ipdb"
version = "0.13.13"
description = "IPython-enabled pdb"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"},
{file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"},
]
[package.dependencies]
decorator = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
[[package]]
name = "ipython"
version = "8.13.2"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
python-versions = ">=3.9"
files = [
{file = "ipython-8.13.2-py3-none-any.whl", hash = "sha256:ffca270240fbd21b06b2974e14a86494d6d29290184e788275f55e0b55914926"},
{file = "ipython-8.13.2.tar.gz", hash = "sha256:7dff3fad32b97f6488e02f87b970f309d082f758d7b7fc252e3b19ee0e432dbb"},
]
[package.dependencies]
appnope = {version = "*", markers = "sys_platform == \"darwin\""}
backcall = "*"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5"
[package.extras]
all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
black = ["black"]
doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
[[package]]
name = "isort"
version = "5.12.0"
@ -1074,6 +1187,26 @@ files = [
{file = "jamo-0.4.1.tar.gz", hash = "sha256:ea65cf9d35338d0e0af48d75ff426d8a369b0ebde6f07051c3ac37256f56d025"},
]
[[package]]
name = "jedi"
version = "0.18.2"
description = "An autocompletion tool for Python that can be used for text editors."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"},
{file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"},
]
[package.dependencies]
parso = ">=0.8.0,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]]
name = "jieba"
version = "0.42.1"
@ -1473,6 +1606,21 @@ pillow = ">=6.2.0"
pyparsing = ">=2.3.1"
python-dateutil = ">=2.7"
[[package]]
name = "matplotlib-inline"
version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
{file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "mccabe"
version = "0.7.0"
@ -2096,6 +2244,49 @@ sql-other = ["SQLAlchemy (>=1.4.16)"]
test = ["hypothesis (>=6.34.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.6.3)"]
[[package]]
name = "parso"
version = "0.8.3"
description = "A Python Parser"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
]
[package.extras]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pexpect"
version = "4.8.0"
description = "Pexpect allows easy control of interactive console applications."
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "pickleshare"
version = "0.7.5"
description = "Tiny 'shelve'-like database with concurrency support"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
[[package]]
name = "pillow"
version = "9.5.0"
@ -2233,6 +2424,21 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "prompt-toolkit"
version = "3.0.38"
description = "Library for building powerful interactive command lines in Python"
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"},
{file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "protobuf"
version = "3.19.6"
@ -2295,6 +2501,33 @@ files = [
[package.extras]
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "pycparser"
version = "2.21"
@ -2307,6 +2540,21 @@ files = [
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
[[package]]
name = "pygments"
version = "2.15.1"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
{file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
]
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "pylint"
version = "2.17.2"
@ -2821,6 +3069,26 @@ numpy = "*"
docs = ["linkify-it-py", "myst-parser", "sphinx", "sphinx-book-theme"]
test = ["pytest"]
[[package]]
name = "stack-data"
version = "0.6.2"
description = "Extract data from python stack frames and tracebacks for informative displays"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"},
{file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "sympy"
version = "1.11.1"
@ -3025,6 +3293,22 @@ all = ["black", "coqpit", "coverage", "fsspec", "isort", "protobuf (>=3.9.2,<3.2
dev = ["black", "coverage", "isort", "pylint (==2.10.2)", "pytest"]
test = ["torchvision"]
[[package]]
name = "traitlets"
version = "5.9.0"
description = "Traitlets Python configuration system"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
{file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
[[package]]
name = "triton"
version = "2.0.0"
@ -3232,6 +3516,18 @@ platformdirs = ">=3.2,<4"
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]]
name = "wcwidth"
version = "0.2.6"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
]
[[package]]
name = "werkzeug"
version = "2.2.3"
@ -3441,4 +3737,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.11"
content-hash = "8d72955bdf3e95e400eb660e7f877c3e2abd29e209a8864979cc0981ad0fda3a"
content-hash = "48f29c37a983f8be8625c486851c41800f35f60b6b2c772863bf48760ed99bf0"

View File

@ -17,6 +17,7 @@ simpleaudio = "^1.0.4"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.2.2"
pylint = "^2.17.2"
ipdb = "^0.13.13"
[build-system]
requires = ["poetry-core"]