diff --git a/meissa/server.py b/meissa/server.py
index 222cb37..da1f3ff 100644
--- a/meissa/server.py
+++ b/meissa/server.py
@@ -13,94 +13,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
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:
diff --git a/meissa/worker.py b/meissa/worker.py
new file mode 100644
index 0000000..e640353
--- /dev/null
+++ b/meissa/worker.py
@@ -0,0 +1,103 @@
+# Meissa - A trainable and simple text to speech server
+#
+# Copyright (c) 2023 Sameer Rahmani
+#
+# 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 .
+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")
diff --git a/poetry.lock b/poetry.lock
index b9fae65..41923c3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -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"
diff --git a/pyproject.toml b/pyproject.toml
index 5a46fa4..44c5f93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]