ソースを参照

first commmit

yanzheng 1 年間 前
コミット
052792938b
100 ファイル変更13537 行追加0 行削除
  1. 170 0
      .gitignore
  2. 31 0
      .pre-commit-config.yaml
  3. 429 0
      .pylintrc
  4. 15 0
      CONTRIBUTING.md
  5. 45 0
      Pipfile
  6. 4158 0
      Pipfile.lock
  7. 146 0
      README.md
  8. 8 0
      backend/.streamlit/config.toml
  9. 2 0
      backend/.streamlit/secrets.toml
  10. 386 0
      backend/apis/version1/route_eeg.py
  11. 76 0
      backend/apis/version1/route_peripheral.py
  12. 11 0
      backend/components/remove_style.py
  13. 0 0
      backend/core/__init__.py
  14. 16 0
      backend/core/mi/feature_extractors.py
  15. 40 0
      backend/core/mi/model.py
  16. 47 0
      backend/core/mi/pipeline.py
  17. 117 0
      backend/core/mi/utils.py
  18. 0 0
      backend/core/peripheral/__init__.py
  19. 21 0
      backend/core/peripheral/factory.py
  20. 37 0
      backend/core/peripheral/hand/base.py
  21. 289 0
      backend/core/peripheral/hand/fubo_mechanical_finger.py
  22. 115 0
      backend/core/peripheral/hand/fubo_pneumatic_finger.py
  23. 443 0
      backend/core/peripheral/hand/ruishou.py
  24. 33 0
      backend/core/peripheral/manager.py
  25. 11 0
      backend/core/sig_chain/device/connector_factory.py
  26. 86 0
      backend/core/sig_chain/device/connector_interface.py
  27. 3 0
      backend/core/sig_chain/device/fake_sig/faker-server-setup.ps1
  28. 180 0
      backend/core/sig_chain/device/fake_sig/sig_fake_server.py
  29. 120 0
      backend/core/sig_chain/device/fake_sig/sig_generator.py
  30. 42 0
      backend/core/sig_chain/device/fake_sig/sig_reader.py
  31. 163 0
      backend/core/sig_chain/device/faker.py
  32. 58 0
      backend/core/sig_chain/device/montage_base_model.py
  33. 171 0
      backend/core/sig_chain/device/neo.py
  34. 323 0
      backend/core/sig_chain/pre_process.py
  35. 166 0
      backend/core/sig_chain/sig_buffer.py
  36. 43 0
      backend/core/sig_chain/sig_reader.py
  37. 154 0
      backend/core/sig_chain/sig_receive.py
  38. 132 0
      backend/core/sig_chain/sig_save.py
  39. 57 0
      backend/core/sig_chain/utils.py
  40. 281 0
      backend/core/utils.py
  41. 3 0
      backend/data/113981_train_2023-11-08_14h40.10.130.csv
  42. BIN
      backend/data/113981_train_2023-11-08_14h40.10.130.psydat
  43. 3 0
      backend/data/136851_train_2023-11-08_14h44.32.460.csv
  44. BIN
      backend/data/136851_train_2023-11-08_14h44.32.460.psydat
  45. 3 0
      backend/data/961814_train_2023-11-08_14h46.13.009.csv
  46. BIN
      backend/data/961814_train_2023-11-08_14h46.13.009.psydat
  47. 22 0
      backend/db/models/subject.py
  48. 22 0
      backend/db/models/train.py
  49. 131 0
      backend/logging.json
  50. 65 0
      backend/main.py
  51. 45 0
      backend/pages/2_train.py
  52. 29 0
      backend/pages/3_test.py
  53. 64 0
      backend/schemas/hand_peripheral.py
  54. 86 0
      backend/schemas/subjects.py
  55. 66 0
      backend/schemas/trains.py
  56. 79 0
      backend/settings/config.py
  57. 214 0
      backend/static/config/config.json
  58. 38 0
      backend/static/config/message_zh.json
  59. 119 0
      backend/tests/core/mi/test_csp.py
  60. 46 0
      backend/tests/core/mi/test_erds.py
  61. 130 0
      backend/tests/core/mi/test_psd.py
  62. 40 0
      backend/tests/core/mi/test_riemannian.py
  63. 40 0
      backend/tests/core/mi/test_wpli.py
  64. 68 0
      backend/tests/core/peripheral/hand/test_fubo_pneumatic_finger.py
  65. 274 0
      backend/tests/core/peripheral/hand/test_ruishou.py
  66. 171 0
      backend/tests/core/sig_chain/device/test_faker.py
  67. 186 0
      backend/tests/core/sig_chain/device/test_neo.py
  68. 580 0
      backend/tests/core/sig_chain/test_pre_process.py
  69. 223 0
      backend/tests/core/sig_chain/test_receive.py
  70. 143 0
      backend/tests/core/sig_chain/test_sig_buffer.py
  71. 33 0
      backend/tests/core/sig_chain/test_sig_reader.py
  72. 108 0
      backend/tests/core/sig_chain/test_sig_save.py
  73. 264 0
      backend/tests/core/test_utils.py
  74. BIN
      backend/tests/data/5_3_right_hand.bdf
  75. 0 0
      backend/tests/data/eeg_raw_data.bdf
  76. BIN
      backend/tests/data/neo_eeg_raw_data.bdf
  77. BIN
      backend/tests/data/normal_side.mp4
  78. 0 0
      backend/tests/utils/__init__.py
  79. 36 0
      backend/tests/utils/core.py
  80. 57 0
      backend/tests/utils/subject.py
  81. 38 0
      backend/tests/utils/train.py
  82. 72 0
      backend/tests/utils/utils.py
  83. 1300 0
      backend/train_1.py
  84. 0 0
      docs/UML/MI_activity.svg
  85. 0 0
      docs/UML/backend_train_detail.svg
  86. 109 0
      docs/UML/dbschema.svg
  87. 2 0
      docs/UML/framework.svg
  88. BIN
      docs/UML/frontend_component.png
  89. 0 0
      docs/UML/overview.svg
  90. 0 0
      docs/UML/page_activity_create_train.svg
  91. 0 0
      docs/UML/page_activity_home.svg
  92. 0 0
      docs/UML/page_activity_prepare_train.svg
  93. 0 0
      docs/UML/page_activity_subject_detail.svg
  94. 0 0
      docs/UML/route_eeg_activity.svg
  95. 0 0
      docs/UML/sig_chain sequence.svg
  96. BIN
      docs/UML/subject_sequence.png
  97. BIN
      docs/UML/train_sequence.png
  98. 3 0
      docs/UML/usecase.svg
  99. 0 0
      docs/UML/外设_fubo_seq.svg
  100. 0 0
      docs/UML/外设_ruishou_seq.svg

+ 170 - 0
.gitignore

@@ -0,0 +1,170 @@
+*.db
+.pytest_cache
+.vscode
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# projects
+backend/db/data/
+backend/static/video/
+backend/static/images/
+backend/logs/
+node_modules/
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/

+ 31 - 0
.pre-commit-config.yaml

@@ -0,0 +1,31 @@
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.2.0
+    hooks:
+    -   id: trailing-whitespace
+        exclude: \.(bdf|svg)$
+    -   id: end-of-file-fixer
+        exclude: \.(bdf|svg)$
+    -   id: check-yaml
+    -   id: check-added-large-files
+        exclude: \.bdf$
+-   repo: https://github.com/commitizen-tools/commitizen
+    rev: v2.40.0
+    hooks:
+    -   id: commitizen
+    -   id: commitizen-branch
+        stages: [push]
+-   repo: local
+    hooks:
+    -   id: pylint
+        name: pylint
+        entry: pylint
+        language: system
+        types: [python]
+        args:
+          [
+            "-rn", # Only display messages
+            "-sn", # Don't display the score
+            "--rcfile=.pylintrc", # Link to your config file
+            "--load-plugins=pylint.extensions.docparams", # Load an extension
+          ]

+ 429 - 0
.pylintrc

@@ -0,0 +1,429 @@
+# This Pylint rcfile contains a best-effort configuration to uphold the
+# best-practices and style described in the Google Python style guide:
+#   https://google.github.io/styleguide/pyguide.html
+#
+# Its canonical open-source location is:
+#   https://google.github.io/styleguide/pylintrc
+
+[MASTER]
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=third_party
+
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
+ignore-patterns=
+
+# Pickle collected data for later comparisons.
+persistent=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Use multiple processes to speed up Pylint.
+jobs=4
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=abstract-method,
+        apply-builtin,
+        arguments-differ,
+        attribute-defined-outside-init,
+        backtick,
+        bad-option-value,
+        basestring-builtin,
+        buffer-builtin,
+        c-extension-no-member,
+        consider-using-enumerate,
+        cmp-builtin,
+        cmp-method,
+        coerce-builtin,
+        coerce-method,
+        delslice-method,
+        div-method,
+        duplicate-code,
+        eq-without-hash,
+        execfile-builtin,
+        file-builtin,
+        filter-builtin-not-iterating,
+        fixme,
+        getslice-method,
+        global-statement,
+        hex-method,
+        idiv-method,
+        implicit-str-concat,
+        import-error,
+        import-self,
+        import-star-module-level,
+        inconsistent-return-statements,
+        input-builtin,
+        intern-builtin,
+        invalid-str-codec,
+        locally-disabled,
+        long-builtin,
+        long-suffix,
+        map-builtin-not-iterating,
+        misplaced-comparison-constant,
+        missing-function-docstring,
+        metaclass-assignment,
+        next-method-called,
+        next-method-defined,
+        no-absolute-import,
+        no-else-break,
+        no-else-continue,
+        no-else-raise,
+        no-else-return,
+        no-init,  # added
+        no-member,
+        no-name-in-module,
+        no-self-use,
+        nonzero-method,
+        oct-method,
+        old-division,
+        old-ne-operator,
+        old-octal-literal,
+        old-raise-syntax,
+        parameter-unpacking,
+        print-statement,
+        raising-string,
+        range-builtin-not-iterating,
+        raw_input-builtin,
+        rdiv-method,
+        reduce-builtin,
+        relative-import,
+        reload-builtin,
+        round-builtin,
+        setslice-method,
+        signature-differs,
+        standarderror-builtin,
+        suppressed-message,
+        sys-max-int,
+        too-few-public-methods,
+        too-many-ancestors,
+        too-many-arguments,
+        too-many-boolean-expressions,
+        too-many-branches,
+        too-many-instance-attributes,
+        too-many-locals,
+        too-many-nested-blocks,
+        too-many-public-methods,
+        too-many-return-statements,
+        too-many-statements,
+        trailing-newlines,
+        unichr-builtin,
+        unicode-builtin,
+        unnecessary-pass,
+        unpacking-in-except,
+        useless-else-on-loop,
+        useless-object-inheritance,
+        useless-suppression,
+        using-cmp-argument,
+        wrong-import-order,
+        xrange-builtin,
+        zip-builtin-not-iterating,
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=main,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
+
+# Regular expression matching correct function names
+function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct constant names
+const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct attribute names
+attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
+
+# Regular expression matching correct argument names
+argument-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=^_?[A-Z][a-zA-Z0-9]*$
+
+# Regular expression matching correct module names
+module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
+
+# Regular expression matching correct method names
+method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
+# lines made too long by directives to pytype.
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=(?x)(
+  ^\s*(\#\ )?<?https?://\S+>?$|
+  ^\s*(from\s+\S+\s+)?import\s+.+$)
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=yes
+
+# Maximum number of lines in a module
+max-module-lines=99999
+
+# String used as indentation unit.  The internal Google style guide mandates 2
+# spaces.  Google's externaly-published style guide says 4, consistent with
+# PEP 8.  Here, we use 2 spaces, for conformity with many open-sourced Google
+# projects (like TensorFlow).
+indent-string='    '
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=TODO
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=yes
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging,absl.logging,tensorflow.io.logging
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+                   TERMIOS,
+                   Bastion,
+                   rexec,
+                   sets
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant, absl
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls,
+                            class_
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=StandardError,
+                       Exception,
+                       BaseException

+ 15 - 0
CONTRIBUTING.md

@@ -0,0 +1,15 @@
+# Contribution guide
+
+[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
+
+## Developing commitizen
+
+```
+pipenv install --dev --ignore-pipfile
+pre-commit install
+```
+然后使用下列命令提交commit信息
+
+```
+cz commit
+```

+ 45 - 0
Pipfile

@@ -0,0 +1,45 @@
+[[source]]
+url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+fastapi = "*"
+uvicorn = "*"
+jinja2 = "*"
+aiofiles = "*"
+sqlalchemy = "*"
+requests = "*"
+flaskwebgui = "*"
+python-multipart = "*"
+deepface = "*"
+mediapipe = "*"
+scipy = "*"
+websockets = "*"
+numpy = "*"
+scikit-learn = "==1.1.3"
+databases = "*"
+py-iir-filter = "*"
+pyedflib = "*"
+av = "*"
+mne = "*"
+func-timeout = "*"
+faker = "*"
+pyserial = "*"
+seaborn = "*"
+mne-connectivity = "*"
+streamlit = "*"
+psychopy = "*"
+
+[dev-packages]
+yapf = "*"
+pylint = "*"
+httpx = "*"
+pyinstaller = "==5.6.2"
+pytest = "==7.2.0"
+cython = "*"
+pre-commit = "*"
+commitizen = "*"
+
+[requires]
+python_full_version = "3.10.11"

+ 4158 - 0
Pipfile.lock

@@ -0,0 +1,4158 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "f4ede0d76b15b29c24bb90188d6689c2ab9f2cf025d63a1c62d907eca4496445"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_full_version": "3.10.11"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.tuna.tsinghua.edu.cn/simple/",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "absl-py": {
+            "hashes": [
+                "sha256:0d3fe606adfa4f7db64792dd4c7aee4ee0c38ab75dfd353b7a83ed3e957fcb47",
+                "sha256:d2c244d01048ba476e7c080bd2c6df5e141d211de80223460d5b3b8a2a58433d"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==1.4.0"
+        },
+        "aiofiles": {
+            "hashes": [
+                "sha256:9312414ae06472eb6f1d163f555e466a23aed1c8f60c30cccf7121dba2e53eb2",
+                "sha256:edd247df9a19e0db16534d4baaf536d6609a43e1de5401d7a4c1c148753a1635"
+            ],
+            "index": "pypi",
+            "version": "==23.1.0"
+        },
+        "altair": {
+            "hashes": [
+                "sha256:7219708ec33c152e53145485040f428954ed15fd09b2a2d89e543e6d111dae7f",
+                "sha256:e5f52a71853a607c61ce93ad4a414b3d486cd0d46ac597a24ae8bd1ac99dd460"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==5.1.2"
+        },
+        "annotated-types": {
+            "hashes": [
+                "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43",
+                "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.6.0"
+        },
+        "anyio": {
+            "hashes": [
+                "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421",
+                "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"
+            ],
+            "markers": "python_full_version >= '3.6.2'",
+            "version": "==3.6.2"
+        },
+        "arabic-reshaper": {
+            "hashes": [
+                "sha256:3f71d5034bb694204a239a6f1ebcf323ac3c5b059de02259235e2016a1a5e2dc",
+                "sha256:ffcd13ba5ec007db71c072f5b23f420da92ac7f268512065d49e790e62237099"
+            ],
+            "version": "==3.0.0"
+        },
+        "astunparse": {
+            "hashes": [
+                "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872",
+                "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"
+            ],
+            "version": "==1.6.3"
+        },
+        "attrs": {
+            "hashes": [
+                "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
+                "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1.0"
+        },
+        "av": {
+            "hashes": [
+                "sha256:04cd0ce13a87870fb0a0ea4673f04934af2b9ac7ae844eafe92e2c19c092ab11",
+                "sha256:0577a38664e453b4ffb63d616a0d23c295827b16ae96a090e89527a753de8718",
+                "sha256:07c971573035d22ce50069d3f2bbdb4d6d02d626ab13db12fda3ce519cda3f22",
+                "sha256:088636ded03724a2ab51136f6f4be0bc457bdb3c0d2ac7158792fe81150d4c1a",
+                "sha256:0f9c88062ebfd2ce547c522b64f79e487ed2b0a6a9d6693c801b28df0d944607",
+                "sha256:10facb5b933551dd6a30d8015bc91eef5d1c864ee86aa3463ffbaff1a99f6c6a",
+                "sha256:115e144d5a1f205378a4b3a3657b7ed3e45918ebe5d2003a891e45984e8f443a",
+                "sha256:1301e4cf1a2c899851073720cd541066c8539b64f9eb0d52216f8d0a59f20429",
+                "sha256:13fe0b48b9211539323ecebbf84154c86c72d16723c6d0af76e29ae5c3a614b2",
+                "sha256:157bde3ffd1615a9006b56e4daf3b46848d3ee2bd46b0394f7568e43ed7ab5a9",
+                "sha256:16bd82b63d0b4c1b855b3c36b13337f7cdc5925bd8284fab893bdf6c290fc3a9",
+                "sha256:1b459ca0ef25c1a0e370112556bdc5b7752f76dc9bd497acaf3e653171e4b946",
+                "sha256:1cdede2325cb750b5bf79238bbf06f9c2a70b757b12726003769a43493b7233a",
+                "sha256:27d6d38c7c8d46d578c008ffcb8aad1eae14d0621fff41f4ad62395589045fe4",
+                "sha256:3dac2a8b0791c3373270e32f6cd27e6b60628565a188e40a5d9660d3aab05e33",
+                "sha256:51037f4bde03daf924236af4f444e17345792ad7f6f70760a5e5863407e14f2b",
+                "sha256:63dbafcd02415127d97509523bc285f1ab260988f87b744d7fb1baee6ffbdf96",
+                "sha256:69fd5a38395191a0f4b71adf31057ff177c9f0762914d73d8797742339ad67d0",
+                "sha256:7a7d6e2b3fbda6464f74fe010dbcff361394bb014b0cb4aa4dc9f2bb713ce882",
+                "sha256:7c579d718b52beb812ea2a7bd68f812d0920b00937804d52d31d41bb71aa5557",
+                "sha256:7dba96a85cd37315529998e6dbbe3fa05c2344eb19a431dc24996be030a904ee",
+                "sha256:81b5264d9752f49286bc1dc4d2cc66187418c4948a326dbed837c766c9892139",
+                "sha256:836d69a9543d284976b229cc8d4343ffcfc0bbaf05239e13fb7e613b13d5291d",
+                "sha256:86bb3f6e8cce62ad18cd34eb2eadd091d99f51b40be81c929b53fbd8fecf6d90",
+                "sha256:8afd3d5610e1086f3b2d8389d66672ea78624516912c93612de64dcaa4c67e05",
+                "sha256:8b6326fd0755761e3ee999e4bf90339e869fe71d548b679fee89157858b8d04a",
+                "sha256:91ea46fea7259abdfabe00b0ed3a9ca18e7fff7ce80d2a2c66a28f797cce838a",
+                "sha256:9788e6e15db0910fb8e1548ba7540799d07066177710590a5794a524c4910e05",
+                "sha256:98cc376199c0aa6e9365d03e0f4e67cfb209e40fe9c0cf566372f9daf2a0c779",
+                "sha256:a2cfd39baa5d82768d2a8898de7bfd450a083ef22b837d57e5dc1b6de3244218",
+                "sha256:a62edd533d330aa61902ae8cd82966affa487fa337a0c4f58ae8866ccb5d31c0",
+                "sha256:a6c8f3f8c26d35eefe45b849c81fd0816ba4b6f589baec7357c25b4c5537d3c4",
+                "sha256:ab930735112c1f788cc4d47c42c59ba0dd214d815aa906e1addf39af91d15194",
+                "sha256:b3fae238751ec0db6377b2106e13762ca84dbe104bd44c1ce9b424163aef4ab5",
+                "sha256:b67b7d028c9cf68215376662fd2e0be6ca0cc02d32d3ed8514fec67b12db9cbd",
+                "sha256:c2eeec7beaebfe9e2213b3c94b482381187d0afdcb632f93239b44dc668b97df",
+                "sha256:ccaf786e747b126a5b3b9a8f5ffbb6a20c5f528775cc7084c95732ca72606fba",
+                "sha256:d19bb54197155d045a2b683d993026d4bcb06e31c2acad0327e3e8711571899c",
+                "sha256:e2ea4424d0be62fe18c843420284a0907bcb38d577062d62c4b75a8e940e6057",
+                "sha256:e5085d11345484c0097898994bb3f515002e7e1deeb43dd11d30dd6f45402c49",
+                "sha256:eba192274538617bbe60097a013d83637f1a5ba9844bbbcf3ca7e43c6499b9d5",
+                "sha256:eebd5aa9d8b1e33e715c5409544a712f13ec805bb0110d75f394ff28d2fb64ad",
+                "sha256:f7b508813abbc100162d305a1ac9b2dd16e5128d56f2ac69639fc6a4b5aca69e",
+                "sha256:ff0f7d3b1003a9ed0d06038f3f521a5ff0d3e056ec5111e2a78e303f98b815a7"
+            ],
+            "index": "pypi",
+            "version": "==10.0.0"
+        },
+        "beautifulsoup4": {
+            "hashes": [
+                "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da",
+                "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"
+            ],
+            "markers": "python_full_version >= '3.6.0'",
+            "version": "==4.12.2"
+        },
+        "blinker": {
+            "hashes": [
+                "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
+                "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.7.0"
+        },
+        "blosc2": {
+            "hashes": [
+                "sha256:0eb8ae893b60743a31feb4ed02dd96039400fb8e7fc5ff4d9adea8d70acde204",
+                "sha256:135afe34913cd43b02186fb400f30e2c9bdbfe3752470d9b6b00a20e7293fb9f",
+                "sha256:25f27b50b2823e6a2e142eff02840979c19f629eb7833b45a98332a2d728543f",
+                "sha256:2f774b0c20b86c99fe1ba4fa7737add60d71930662192fdf66a547707a1e3a37",
+                "sha256:368b12e43249e55137a05506e747cc4656539afc73bf82a85b896a2f13a529d8",
+                "sha256:49f3b3951764ddf6d7ad3c1c0800adef2b7780348b1fe5126b6e0970f3ea6c2f",
+                "sha256:555468f4c77a45e35a7a878fab7679bf4705585a84b81649fc423eba293cf17b",
+                "sha256:562828192e3c6f4629823d836bec1d129dfdad38a7e6d2e84f52dcaf9979633b",
+                "sha256:5a4db24030be00e8ccc9ff0645716504e4caf7525b70c7976ad8434b47f04f4f",
+                "sha256:5f9413d6926d7442847b115680567fd4ad4ddcdf46e2419cd2f5e82ee8d00f6c",
+                "sha256:63606498aaa72d58215b618d4512d5d3de29000a7b01a870edce8cb21d237c40",
+                "sha256:658443f639975d29eaa3feea269a2f971d2da5cab736bb6462561d7efe261cc3",
+                "sha256:73c7a7afd5390d60ad8ecd1e0e5de2492c60a24cce748b8ae2da83ceda0649ad",
+                "sha256:74a24b4efb8b608b71d8af51d5c8f16dc63f45c2145240e7d313472fa720a68e",
+                "sha256:7746244318adeb552cfb45c95b329eb12e146159ae6506b06b4854dec4c3b2c1",
+                "sha256:82ec6d1a4343868ce833380c82f60e9799794e04d35f630af948f0f3d28c3577",
+                "sha256:8504a92404b2ba5112db83bebdfbe7eb3c286514acb658191434f020ea084c7a",
+                "sha256:97e788170a2e80cac38f15d723f7397a87d3c522980fc4f8d96c6fa9f5a74dd3",
+                "sha256:a46f9216d63958572514354b94eaedaa2052b60b3301ec7c41c8f30c6825c718",
+                "sha256:adaef04627713e22bc7883a35afd499266762f700d8644a65cfafbf2879d4350",
+                "sha256:c11ace31c542aa6eed11708e7b92cf5d3dbbb3c1b8a691919c3bb6130caf1746",
+                "sha256:c840bdfd97e25cd61d6e048f8d9ee6478133f3e70c880c2cb3054db93e142bba",
+                "sha256:e24335d97ae43558d222b15141d8499c3b220b3d166350441a6d2a4470997921",
+                "sha256:e38cc441798595f05e70d620f1124cd4c472003f9b58c17e79dd0477a4d151fb",
+                "sha256:e82b6280107b9ec05aa0ae7d86a3f73d14bd99767901cec95dab622d37cb0d7e",
+                "sha256:ebfc1e9736d83bffa16e49f53278de6caa7b5469c44a4448800fc40009efbbba",
+                "sha256:f10e14c7f3b9f14431df58f9891e490af83ae6fb3d7c2a7d05722560273a2da8",
+                "sha256:f31c0ee147f5f78ceeb65b601c47b0431a0f6111b8443aeb1485547394725895",
+                "sha256:fa36fa18b8d41aee7db975a318b481304e6e3558b48641ec53933287274a4ec3"
+            ],
+            "markers": "python_version >= '3.8' and python_version < '4'",
+            "version": "==2.2.9"
+        },
+        "cachetools": {
+            "hashes": [
+                "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14",
+                "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"
+            ],
+            "markers": "python_version ~= '3.7'",
+            "version": "==5.3.0"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
+                "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2022.12.7"
+        },
+        "cffi": {
+            "hashes": [
+                "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc",
+                "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a",
+                "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417",
+                "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab",
+                "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520",
+                "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36",
+                "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743",
+                "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8",
+                "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed",
+                "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684",
+                "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56",
+                "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324",
+                "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d",
+                "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235",
+                "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e",
+                "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088",
+                "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000",
+                "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7",
+                "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e",
+                "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673",
+                "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c",
+                "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe",
+                "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2",
+                "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098",
+                "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8",
+                "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a",
+                "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0",
+                "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b",
+                "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896",
+                "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e",
+                "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9",
+                "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2",
+                "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b",
+                "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6",
+                "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404",
+                "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f",
+                "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0",
+                "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4",
+                "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc",
+                "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936",
+                "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba",
+                "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872",
+                "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb",
+                "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614",
+                "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1",
+                "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d",
+                "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969",
+                "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b",
+                "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4",
+                "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627",
+                "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
+                "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.16.0"
+        },
+        "cftime": {
+            "hashes": [
+                "sha256:055d5d60a756c6c1857cf84d77655bb707057bb6c4a4fbb104a550e76c40aad9",
+                "sha256:07fdef2f75a0f0952b0376fa4cd08ef8a1dad3b963976ac07517811d434936b7",
+                "sha256:0955e1f3e1c09a9e0296b50f135ff9719cb2466f81c8ad4a10ef06fa394de984",
+                "sha256:29c18601abea0fd160fbe423e05c7a56fe1d38dd250a6b010de499a132d3fe18",
+                "sha256:2abdac6ca5b8b6102f319122546739dfc42406b816c16f2a98a8f0cd406d3bf0",
+                "sha256:2ba7909a0cd4adcb16797d8d6ab2767e7ddb980b2bf9dbabfc71b3bdd94f072b",
+                "sha256:3042048324b4d6a1066c978ec78101effdd84320e8862bfdbf8122d7ad7588ec",
+                "sha256:455cec3627e6ca8694b0d9201da6581eb4381b58389f1fbcb51a14fa0e2b3d94",
+                "sha256:56d0242fc4990584b265622622b25bb262a178097711d2d95e53ef52a9d23e7e",
+                "sha256:8614c00fb8a5046de304fdd86dbd224f99408185d7b245ac6628d0276596e6d2",
+                "sha256:86fe550b94525c327578a90b2e13418ca5ba6c636d5efe3edec310e631757eea",
+                "sha256:892d5dc38f8b998c83a2a01f131e63896d020586de473e1878f9e85acc70ad44",
+                "sha256:8d49d69c64cee2c175478eed84c3a57fce083da4ceebce16440f72be561a8489",
+                "sha256:93f00f454329c1f2588ebca2650e8edf7607d6189dbdcc81b5f3be2080155cc4",
+                "sha256:acb294fdb80e33545ae54b4421df35c4e578708a5ffce1c00408b2294e70ecef",
+                "sha256:aedfb7a783d19d7a30cb41951310f3bfe98f9f21fffc723c8af08a11962b0b17",
+                "sha256:afb5b38b51b8bc02f1656a9f15c52b0b20a3999adbe1ab9ac57f926e0065b48a",
+                "sha256:b4d2a1920f0aad663f25700b30621ff64af373499e52b544da1148dd8c09409a",
+                "sha256:e83db2fdda900eb154a9f79dfb665ac6190781c61d2e18151996de5ee7ffd8a2",
+                "sha256:eb7f8cd0996640b83020133b5ef6b97fc9216c3129eaeeaca361abdff5d82166",
+                "sha256:ee70fa069802652cf534de1dd3fc590b7d22d4127447bf96ac9849abcdadadf1"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.6.2"
+        },
+        "charset-normalizer": {
+            "hashes": [
+                "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6",
+                "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1",
+                "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e",
+                "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373",
+                "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62",
+                "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230",
+                "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be",
+                "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c",
+                "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0",
+                "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448",
+                "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f",
+                "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649",
+                "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d",
+                "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0",
+                "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706",
+                "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a",
+                "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59",
+                "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23",
+                "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5",
+                "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb",
+                "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e",
+                "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e",
+                "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c",
+                "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28",
+                "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d",
+                "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41",
+                "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974",
+                "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce",
+                "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f",
+                "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1",
+                "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d",
+                "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8",
+                "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017",
+                "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31",
+                "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7",
+                "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8",
+                "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e",
+                "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14",
+                "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd",
+                "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d",
+                "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795",
+                "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b",
+                "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b",
+                "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b",
+                "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203",
+                "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f",
+                "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19",
+                "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1",
+                "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a",
+                "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac",
+                "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9",
+                "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0",
+                "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137",
+                "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f",
+                "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6",
+                "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5",
+                "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909",
+                "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f",
+                "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0",
+                "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324",
+                "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755",
+                "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb",
+                "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854",
+                "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c",
+                "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60",
+                "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84",
+                "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0",
+                "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b",
+                "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1",
+                "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531",
+                "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1",
+                "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11",
+                "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326",
+                "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df",
+                "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==3.1.0"
+        },
+        "click": {
+            "hashes": [
+                "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
+                "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.3"
+        },
+        "colorama": {
+            "hashes": [
+                "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
+                "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==0.4.6"
+        },
+        "contourpy": {
+            "hashes": [
+                "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98",
+                "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772",
+                "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2",
+                "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc",
+                "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803",
+                "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051",
+                "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc",
+                "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4",
+                "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436",
+                "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5",
+                "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5",
+                "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3",
+                "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80",
+                "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1",
+                "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0",
+                "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae",
+                "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556",
+                "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02",
+                "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566",
+                "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350",
+                "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967",
+                "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4",
+                "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66",
+                "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69",
+                "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd",
+                "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2",
+                "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810",
+                "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50",
+                "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc",
+                "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2",
+                "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0",
+                "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3",
+                "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6",
+                "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac",
+                "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d",
+                "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6",
+                "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f",
+                "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd",
+                "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566",
+                "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa",
+                "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414",
+                "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a",
+                "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c",
+                "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693",
+                "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d",
+                "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161",
+                "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e",
+                "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2",
+                "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f",
+                "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71",
+                "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd",
+                "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9",
+                "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8",
+                "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab",
+                "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.0.7"
+        },
+        "cryptography": {
+            "hashes": [
+                "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf",
+                "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84",
+                "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e",
+                "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8",
+                "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7",
+                "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1",
+                "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88",
+                "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86",
+                "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179",
+                "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81",
+                "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20",
+                "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548",
+                "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d",
+                "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d",
+                "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5",
+                "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1",
+                "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147",
+                "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936",
+                "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797",
+                "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696",
+                "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72",
+                "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da",
+                "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==41.0.5"
+        },
+        "cycler": {
+            "hashes": [
+                "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3",
+                "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.11.0"
+        },
+        "databases": {
+            "hashes": [
+                "sha256:cf5da4b8a3e3cd038c459529725ebb64931cbbb7a091102664f20ef8f6cefd0d",
+                "sha256:ea2d419d3d2eb80595b7ceb8f282056f080af62efe2fb9bcd83562f93ec4b674"
+            ],
+            "index": "pypi",
+            "version": "==0.7.0"
+        },
+        "decorator": {
+            "hashes": [
+                "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
+                "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==5.1.1"
+        },
+        "deepface": {
+            "hashes": [
+                "sha256:a6b3d3fdec239e283d612778f541b6aeeb68091111b8b624120ee536d91a4b94",
+                "sha256:e88b6043b55309932b81c32c22321652dc937a040791d63902c9824c1696dd68"
+            ],
+            "index": "pypi",
+            "version": "==0.0.79"
+        },
+        "dukpy": {
+            "hashes": [
+                "sha256:0636f4658024033427907b3a67b9bbc9c405fd7ee1f924ec1b1eca070d7a6efb",
+                "sha256:09ae9309490fc578da96611fc50e46f02c32616e53f55f2bc9c864f67e6c759e",
+                "sha256:0aebd4be1109e58126ff4e959415f3198390b92dc48cf6144b97caeb786cf0df",
+                "sha256:0c6ebf8e495f9750f2820cdedfe384621369ebef562ed52770d7a9f070e5991e",
+                "sha256:1005579d0e3fd7ab35e18138da3172baa59e3305f22a55fbe9961c67204b1ddd",
+                "sha256:1185f27c920889a41e189ba8a2c76211ee84be1ea1bb4c1f7cc4343f9a1a3d2c",
+                "sha256:157280c79833f223f3dc6effe8981525e68cd262e26947b2cddd57addac9a3d8",
+                "sha256:1628e9171e900d2b97e45a22709e68f91bb88ef8dbabfc0c1f4f92524eeb900e",
+                "sha256:1a8df866eb0af6b55f1a27786f5217334a4e904fd04b7c285c4ee5b684072abe",
+                "sha256:1cb574f3b71545adbccbb7688059b1a63eca057c59ac00004a6de196eb95844a",
+                "sha256:218c26430b424cd2fa4a8a0e252acf835719ee2107937d01c7bbc15615b07e0d",
+                "sha256:2c1576a480656ee4bce9bb2f471623b894c8ab809617bdf08b8f547a990df063",
+                "sha256:2edf126a5c8da0b1ffc39381323d3129cf922d041c74c78402652c9efdf74c99",
+                "sha256:2feb5c2d05b3c9b8fafc9088c5c025a14c9e239f96abb1aa75ebc022f1777e9c",
+                "sha256:3698f35c184b3319257d4d7bfa796ef109e8f78fc3cef8e22a3bf0f2d0eef774",
+                "sha256:3bee97f928e0477a197fcc66f25a8d46d1ebc7068ddda2f657445cced303111b",
+                "sha256:3d1f25e485a77e1318b95db43717454001e412adec0ba268dfc8eecf3b893d45",
+                "sha256:47ed8813baf52ad3e3a7d4c7416173af0693bbfab1f3b685cbf0165e0e376769",
+                "sha256:49f6390bbc47b618fdb19d7af89e73f643f308a2ab9f5d5e0eb161d4508f23c6",
+                "sha256:4f0f517d245b69781ad91dcb6d9d1a9550b2dbb0d8b636b9e8899838780ad211",
+                "sha256:531db11c50326c1baa00711a8221995ec0935418c690d02a84ef9ce537968686",
+                "sha256:581bbd180a7d69149a1b3171d987a8d1eedf988ce3d138ca2e1730888012e41a",
+                "sha256:5a1614b73884c14a00b496384d2e793bfd07dfcac425eb1fe768e5b870118111",
+                "sha256:5b895adaab9feaec6e33ba221bcfc16bd50710b18346077b8cec06e843355fb6",
+                "sha256:6acc3a3ce997aef380f79f1985636d87701c1841707c0748ee5eff65e396f0b2",
+                "sha256:6ae877b9d439941e2afcaaaa410ef168c51e885f99665bf591b97a71eafaeb0f",
+                "sha256:6b2ef5b42a666d4cd73618dce1b9b182c02f15cf52598aef4047e0ecbed2f4ed",
+                "sha256:6e16d07a506e79af132a7d1b4d28b7846d1e980a8a965130bfe755f56922f35e",
+                "sha256:74e0a194e8908bfa64ea2e2e353cf28184d498ed675174a96d948ac2dd6db24e",
+                "sha256:7708973d15bc01c91e68195338f9db0a6d4b1e663e2a778da2db00b8c27e7488",
+                "sha256:782e60979db86f7ae9d5e84185cf6c252954cbcfda982353dd30ff6a17fef0be",
+                "sha256:87202891b5dd85053321b561173ebbe84ceab58f9cd4e6c028686e5793bfc976",
+                "sha256:87a9ea4cb2593220e0c6abc6a0b5849e940de78c1e464fe6a4339efe655fd3af",
+                "sha256:9087a3321649beb17f91afa6ffde991d477aa0029c3be5ce908369517ac85251",
+                "sha256:99f76adf6f9c40b0501d7fffc1570a7b7dc4eaf8b2d3cb38ac738068ba2731e6",
+                "sha256:9e044f3e78881f3c1fc0b939349551a9be2e2519d4e670038ce497d7cc780c69",
+                "sha256:a3da5a1bc3ce7788ed05cc16fc67f9be5e187ed4f6fedcf1fd6574633a5230be",
+                "sha256:a60d5b3537800944cb6e8bedbf68a724dea92a6f9a8ce9a48530838e68478716",
+                "sha256:a8171990e640625ad5876a0548072220ebd34c9f0705510144082ce34a2e777b",
+                "sha256:a8e06c2402031e030924088b37bbde27cf43936bf8ff0ff65c9bdfd9bf4ae89c",
+                "sha256:a95ff658b7400e71acaab453359ea74d1a1625cebb937d0294a053b6aac3e507",
+                "sha256:a96a600ce653c5fb9c7190af8c1e82b7d212709dfdd31ce65a2e328cbd923dd6",
+                "sha256:b4b068796a4d81c37673e9d949a6307dbadc2cd6c2062b6010bd6561a24895fb",
+                "sha256:bd7e8b90590122b92e8057052e485afdcc4a6145e50036cc55deac045dd6568f",
+                "sha256:bd7f6ded168548d808e3a3ac97ccf98ee1a97c327e7e67c13229932f3c923f85",
+                "sha256:c072d28ff58db698eb5bfa4556f59e5ce4d4f219b176c93375bfda87c117253f",
+                "sha256:c1827f1f7282bb0cc329c7f687c0f58d87f5736777e553f483c26636e9bd1960",
+                "sha256:c189ae4b5c5deb2b576cd0b0ae0193dbd7e15a1499491b3798f3ed7aae8274b1",
+                "sha256:ca5772e9373f3cf7772a711e65db765c4361dcf6d4e65c5d88cb879e9ee3f5a6",
+                "sha256:cf7412d2d6883fe0ff498cbdb0e67e16804972cf216c169d83aa8d5bad50d109",
+                "sha256:d10c0cd5035e3e2dc27d193734537546f1910d2dc0ccd468bb510924313bbaa2",
+                "sha256:d4b3a69977d89c83d74e64a5feb7264acb007c251e2eb83bc4e79c818b73b4fc",
+                "sha256:d87b932a387d4015d9acdb99b94c788453b19b5aa5fd10584098e042d8c7118a",
+                "sha256:d9697701eb1a01c0044479b3fa501685adc1a699ffe1acbb39b0b724bc1a7bac",
+                "sha256:e31213f8cbbf85d0386f0ea0e478cd0e4dd918a8747d568a6936044dbd21330c",
+                "sha256:e59a93c819cb818251e7d8ad0b548163227fec3b8485c4cdcecfac59abd9db87",
+                "sha256:eca56334b67370427c503b65f21424d317b7560620e28809b4852828a9fcea54",
+                "sha256:f1329be71ce19fdda899a0b59cd531b711adc0d30867488f7401b38b518415a9",
+                "sha256:ff24928cf9c14af226cf575640e2166611a79d8fd14ea498183ca7cd7ab349e5"
+            ],
+            "version": "==0.3.0"
+        },
+        "elementpath": {
+            "hashes": [
+                "sha256:2ac1a2fb31eb22bbbf817f8cf6752f844513216263f0e3892c8e79782fe4bb55",
+                "sha256:c2d6dc524b29ef751ecfc416b0627668119d8812441c555d7471da41d4bacb8d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.1.5"
+        },
+        "esprima": {
+            "hashes": [
+                "sha256:08db1a876d3c2910db9cfaeb83108193af5411fc3a3a66ebefacd390d21323ee"
+            ],
+            "version": "==4.0.1"
+        },
+        "et-xmlfile": {
+            "hashes": [
+                "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c",
+                "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==1.1.0"
+        },
+        "exceptiongroup": {
+            "hashes": [
+                "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+                "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==1.1.3"
+        },
+        "faker": {
+            "hashes": [
+                "sha256:170ead9d0d140916168b142df69c44722b8f622ced2070802d0af9c476f0cb84",
+                "sha256:977ad0b7aa7a61ed57287d6a0723a827e9d3dd1f8cc82aaf08707f281b33bacc"
+            ],
+            "index": "pypi",
+            "version": "==18.4.0"
+        },
+        "fastapi": {
+            "hashes": [
+                "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5",
+                "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"
+            ],
+            "index": "pypi",
+            "version": "==0.95.1"
+        },
+        "ffpyplayer": {
+            "hashes": [
+                "sha256:003dae09ffe6ee053175e3c1b0b2066037cf69ba7f8989e5296fc710c5836ad9",
+                "sha256:154783a6df5bf432962b12586f8dcaae0a0cfcac7fc221b00b00f4aec63fa5fb",
+                "sha256:17656d43f3fe1cfc3b2fd45151ee2f8ab647fa74c58f2650d5891898ab014db5",
+                "sha256:23bc75bc329bc292029af9e0b9a142a174e6ab63ac68035896343f8bd218b880",
+                "sha256:2c7a037af7c4582c00cdf1dc5dc951f8cb963ea375366f52fcffaa0fd063ee86",
+                "sha256:311a2b5c4ef3854d6bfe8af889603cd47ddf8c3e9ff175800b66b1f410aeb915",
+                "sha256:36dbf4d15f7ecc874983bd23895ca143e0d11f38246bca31227716172a31d2c5",
+                "sha256:45e65dd436b879fce4cee40253ff2e083ceec3630ff782ecd083f27a0d67e3f0",
+                "sha256:499b7577499b9eb4b3e0b1462e7aa9fb7a94d11223a12ab7b92e2b34972da587",
+                "sha256:4c916e82c176d45826f4c838636d2007017354959589ef3d6f7afc3cccdac504",
+                "sha256:526853ad4a382d07c89abdddd882188c025a8fe1f4a9d7df81e52cf7973a0cec",
+                "sha256:52b2c436934b4a3a64c2b14abc61a3f3a48193295bdfabf6000da8f05fc8bad1",
+                "sha256:5bfc079ed4481fa0b182433e861e1b22e740e92568f88a9ffabc3c821f6e01b4",
+                "sha256:6129d710c5ac4cfb302d6e1e64fc732175dc467e098fff52835f6efcfbb4d6ab",
+                "sha256:648cd55b621d41a9bb931d0f2066a775dd2f0a26fcfebbe5df2b5c02555a7e83",
+                "sha256:72aca90146d4276d89bd19a50d544ff2939260fd656a30c9b412598358d9fbc8",
+                "sha256:74e82e41b1a955ac91b4e08191002acf9d944740f8ba60cb6ac967d739dc1794",
+                "sha256:86920b16736d361b81f780ac8e1441f91966b5f1d1abe275981382c8cf8f0487",
+                "sha256:a507da4d9be87e9025764d6b6c0dbae92af51053e02e76df23fe5b5ab3b142c2",
+                "sha256:abcbc181fa8685cab0e1db47c51154d8c809dafff59c7d8a37f69a27840f3bd5",
+                "sha256:b234b87f8872e7cb0fae5447422a36adbc331bab5a1552290e7bef29b2acf70b",
+                "sha256:c2b76eee9dcd12fa67b04650d34c3eff5793f889de126867694f17f2553606bf",
+                "sha256:d3a7baeb040891e67a71fd5969f5f56aa6114ac23a2997ac29fb6af145ba9377",
+                "sha256:d55d54563fcf6c334505b45d4b64825f24fafb5acfa7a9e472c08e877f549069",
+                "sha256:d719f663fe1aae0ee97c3fde0fbe626719523d6b5d51424bfdd89a36f159b936",
+                "sha256:ee9851c69a4dca99c79587685689bcd1c8ffc5a371244847e28450982f68f347",
+                "sha256:f629900b0b5717ef935380a8439327e8b1e73577252632acfb32d7367754499b",
+                "sha256:ff2647873d959353ba2323295a08ea34544a2bab80cd9b4b872af5f94ee0e39b",
+                "sha256:ff26d65dd6f4d9fde883aed1006e2818852c06d2deaec05fb796b37d2a1a0c16"
+            ],
+            "version": "==4.5.1"
+        },
+        "filelock": {
+            "hashes": [
+                "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37",
+                "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.11.0"
+        },
+        "fire": {
+            "hashes": [
+                "sha256:a6b0d49e98c8963910021f92bba66f65ab440da2982b78eb1bbf95a0a34aacc6"
+            ],
+            "version": "==0.5.0"
+        },
+        "flask": {
+            "hashes": [
+                "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d",
+                "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.2.3"
+        },
+        "flaskwebgui": {
+            "hashes": [
+                "sha256:6493a7bc172319e79d0a57c3ff288a692637b80fc2708797e0f258e958413848",
+                "sha256:bb154e7b01935f4cf0cd920404a5f6b481001d05e1fa5574847ff3285d6f40ea"
+            ],
+            "index": "pypi",
+            "version": "==1.0.4"
+        },
+        "flatbuffers": {
+            "hashes": [
+                "sha256:5ad36d376240090757e8f0a2cfaf6abcc81c6536c0dc988060375fd0899121f8",
+                "sha256:cabd87c4882f37840f6081f094b2c5bc28cefc2a6357732746936d055ab45c3d"
+            ],
+            "version": "==23.3.3"
+        },
+        "fonttools": {
+            "hashes": [
+                "sha256:64c0c05c337f826183637570ac5ab49ee220eec66cf50248e8df527edfa95aeb",
+                "sha256:9234b9f57b74e31b192c3fc32ef1a40750a8fbc1cd9837a7b7bfc4ca4a5c51d7"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.39.3"
+        },
+        "freetype-py": {
+            "hashes": [
+                "sha256:3e0f5a91bc812f42d98a92137e86bac4ed037a29e43dafdb76d716d5732189e8",
+                "sha256:8ad81195d2f8f339aba61700cebfbd77defad149c51f59b75a2a5e37833ae12e",
+                "sha256:9614f68876e9c62e821dfa811dd6160e00279d9d98cf60118cb264be48da1472",
+                "sha256:a2620788d4f0c00bd75fee2dfca61635ab0da856131598c96e2355d5257f70e5",
+                "sha256:c6276d92ac401c8ce02ea391fc854de413b01a8d835fb394ee5eb6f04fc947f5",
+                "sha256:c9a3abc277f5f6d21575c0093c0c6139c161bf05b91aa6258505ab27c5001c5e",
+                "sha256:ce931f581d5038c4fea1f3d314254e0264e92441a5fdaef6817fe77b7bb888d3"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.4.0"
+        },
+        "func-timeout": {
+            "hashes": [
+                "sha256:74cd3c428ec94f4edfba81f9b2f14904846d5ffccc27c92433b8b5939b5575dd"
+            ],
+            "index": "pypi",
+            "version": "==4.3.5"
+        },
+        "future": {
+            "hashes": [
+                "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.18.3"
+        },
+        "gast": {
+            "hashes": [
+                "sha256:40feb7b8b8434785585ab224d1568b857edb18297e5a3047f1ba012bc83b42c1",
+                "sha256:b7adcdd5adbebf1adf17378da5ba3f543684dbec47b1cda1f3997e573cd542c4"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.4.0"
+        },
+        "gdown": {
+            "hashes": [
+                "sha256:347f23769679aaf7efa73e5655270fcda8ca56be65eb84a4a21d143989541045",
+                "sha256:65d495699e7c2c61af0d0e9c32748fb4f79abaf80d747a87456c7be14aac2560"
+            ],
+            "version": "==4.7.1"
+        },
+        "gevent": {
+            "hashes": [
+                "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a",
+                "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2",
+                "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535",
+                "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e",
+                "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653",
+                "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1",
+                "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c",
+                "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648",
+                "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599",
+                "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea",
+                "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6",
+                "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f",
+                "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9",
+                "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e",
+                "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34",
+                "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397",
+                "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507",
+                "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b",
+                "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd",
+                "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe",
+                "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a",
+                "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b",
+                "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771",
+                "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e",
+                "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69",
+                "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a",
+                "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011",
+                "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7",
+                "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71",
+                "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5",
+                "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae",
+                "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7",
+                "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39",
+                "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d",
+                "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599",
+                "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07",
+                "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904",
+                "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a",
+                "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543",
+                "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==23.9.1"
+        },
+        "gitdb": {
+            "hashes": [
+                "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4",
+                "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.0.11"
+        },
+        "gitpython": {
+            "hashes": [
+                "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4",
+                "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.40"
+        },
+        "google-auth": {
+            "hashes": [
+                "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc",
+                "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+            "version": "==2.17.3"
+        },
+        "google-auth-oauthlib": {
+            "hashes": [
+                "sha256:95880ca704928c300f48194d1770cf5b1462835b6e49db61445a520f793fd5fb",
+                "sha256:e375064964820b47221a7e1b7ee1fd77051b6323c3f9e3e19785f78ab67ecfc5"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==1.0.0"
+        },
+        "google-pasta": {
+            "hashes": [
+                "sha256:4612951da876b1a10fe3960d7226f0c7682cf901e16ac06e473b267a5afa8954",
+                "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed",
+                "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e"
+            ],
+            "version": "==0.2.0"
+        },
+        "greenlet": {
+            "hashes": [
+                "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a",
+                "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a",
+                "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43",
+                "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33",
+                "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8",
+                "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088",
+                "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca",
+                "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343",
+                "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645",
+                "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db",
+                "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df",
+                "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3",
+                "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86",
+                "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2",
+                "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a",
+                "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf",
+                "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7",
+                "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394",
+                "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40",
+                "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3",
+                "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6",
+                "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74",
+                "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0",
+                "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3",
+                "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91",
+                "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5",
+                "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9",
+                "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8",
+                "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b",
+                "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6",
+                "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb",
+                "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73",
+                "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b",
+                "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df",
+                "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9",
+                "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f",
+                "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0",
+                "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857",
+                "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a",
+                "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249",
+                "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30",
+                "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292",
+                "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b",
+                "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d",
+                "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b",
+                "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c",
+                "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca",
+                "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7",
+                "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75",
+                "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae",
+                "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b",
+                "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470",
+                "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564",
+                "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9",
+                "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099",
+                "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0",
+                "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5",
+                "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19",
+                "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1",
+                "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"
+            ],
+            "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
+            "version": "==2.0.2"
+        },
+        "grpcio": {
+            "hashes": [
+                "sha256:0698c094688a2dd4c7c2f2c0e3e142cac439a64d1cef6904c97f6cde38ba422f",
+                "sha256:104a2210edd3776c38448b4f76c2f16e527adafbde171fc72a8a32976c20abc7",
+                "sha256:14817de09317dd7d3fbc8272864288320739973ef0f4b56bf2c0032349da8cdf",
+                "sha256:1948539ce78805d4e6256ab0e048ec793956d54787dc9d6777df71c1d19c7f81",
+                "sha256:19caa5b7282a89b799e63776ff602bb39604f7ca98db6df27e2de06756ae86c3",
+                "sha256:1b172e6d497191940c4b8d75b53de82dc252e15b61de2951d577ec5b43316b29",
+                "sha256:1c734a2d4843e4e14ececf5600c3c4750990ec319e1299db7e4f0d02c25c1467",
+                "sha256:2a912397eb8d23c177d6d64e3c8bc46b8a1c7680b090d9f13a640b104aaec77c",
+                "sha256:2eddaae8af625e45b5c8500dcca1043264d751a6872cde2eda5022df8a336959",
+                "sha256:55930c56b8f5b347d6c8c609cc341949a97e176c90f5cbb01d148d778f3bbd23",
+                "sha256:658ffe1e39171be00490db5bd3b966f79634ac4215a1eb9a85c6cd6783bf7f6e",
+                "sha256:6601d812105583948ab9c6e403a7e2dba6e387cc678c010e74f2d6d589d1d1b3",
+                "sha256:6b6d60b0958be711bab047e9f4df5dbbc40367955f8651232bfdcdd21450b9ab",
+                "sha256:6beb84f83360ff29a3654f43f251ec11b809dcb5524b698d711550243debd289",
+                "sha256:752d2949b40e12e6ad3ed8cc552a65b54d226504f6b1fb67cab2ccee502cc06f",
+                "sha256:7dc8584ca6c015ad82e186e82f4c0fe977394588f66b8ecfc4ec873285314619",
+                "sha256:82434ba3a5935e47908bc861ce1ebc43c2edfc1001d235d6e31e5d3ed55815f7",
+                "sha256:8270d1dc2c98ab57e6dbf36fa187db8df4c036f04a398e5d5e25b4e01a766d70",
+                "sha256:8a48fd3a7222be226bb86b7b413ad248f17f3101a524018cdc4562eeae1eb2a3",
+                "sha256:95952d3fe795b06af29bb8ec7bbf3342cdd867fc17b77cc25e6733d23fa6c519",
+                "sha256:976a7f24eb213e8429cab78d5e120500dfcdeb01041f1f5a77b17b9101902615",
+                "sha256:9c84a481451e7174f3a764a44150f93b041ab51045aa33d7b5b68b6979114e48",
+                "sha256:a34d6e905f071f9b945cabbcc776e2055de1fdb59cd13683d9aa0a8f265b5bf9",
+                "sha256:a4952899b4931a6ba12951f9a141ef3e74ff8a6ec9aa2dc602afa40f63595e33",
+                "sha256:a96c3c7f564b263c5d7c0e49a337166c8611e89c4c919f66dba7b9a84abad137",
+                "sha256:aef7d30242409c3aa5839b501e877e453a2c8d3759ca8230dd5a21cda029f046",
+                "sha256:b5bd026ac928c96cc23149e6ef79183125542062eb6d1ccec34c0a37e02255e7",
+                "sha256:b6a2ead3de3b2d53119d473aa2f224030257ef33af1e4ddabd4afee1dea5f04c",
+                "sha256:ba074af9ca268ad7b05d3fc2b920b5fb3c083da94ab63637aaf67f4f71ecb755",
+                "sha256:c5fb6f3d7824696c1c9f2ad36ddb080ba5a86f2d929ef712d511b4d9972d3d27",
+                "sha256:c705e0c21acb0e8478a00e7e773ad0ecdb34bd0e4adc282d3d2f51ba3961aac7",
+                "sha256:c7ad9fbedb93f331c2e9054e202e95cf825b885811f1bcbbdfdc301e451442db",
+                "sha256:da95778d37be8e4e9afca771a83424f892296f5dfb2a100eda2571a1d8bbc0dc",
+                "sha256:dad5b302a4c21c604d88a5d441973f320134e6ff6a84ecef9c1139e5ffd466f6",
+                "sha256:dbc1ba968639c1d23476f75c356e549e7bbf2d8d6688717dcab5290e88e8482b",
+                "sha256:ddb2511fbbb440ed9e5c9a4b9b870f2ed649b7715859fd6f2ebc585ee85c0364",
+                "sha256:df9ba1183b3f649210788cf80c239041dddcb375d6142d8bccafcfdf549522cd",
+                "sha256:e4f513d63df6336fd84b74b701f17d1bb3b64e9d78a6ed5b5e8a198bbbe8bbfa",
+                "sha256:e6f90698b5d1c5dd7b3236cd1fa959d7b80e17923f918d5be020b65f1c78b173",
+                "sha256:eaf8e3b97caaf9415227a3c6ca5aa8d800fecadd526538d2bf8f11af783f1550",
+                "sha256:ee81349411648d1abc94095c68cd25e3c2812e4e0367f9a9355be1e804a5135c",
+                "sha256:f144a790f14c51b8a8e591eb5af40507ffee45ea6b818c2482f0457fec2e1a2e",
+                "sha256:f3e837d29f0e1b9d6e7b29d569e2e9b0da61889e41879832ea15569c251c303a",
+                "sha256:fa8eaac75d3107e3f5465f2c9e3bbd13db21790c6e45b7de1756eba16b050aca",
+                "sha256:fdc6191587de410a184550d4143e2b24a14df495c86ca15e59508710681690ac"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.53.0"
+        },
+        "gunicorn": {
+            "hashes": [
+                "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
+                "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==20.1.0"
+        },
+        "h11": {
+            "hashes": [
+                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.14.0"
+        },
+        "h5py": {
+            "hashes": [
+                "sha256:03890b1c123d024fb0239a3279737d5432498c1901c354f8b10d8221d1d16235",
+                "sha256:0fef76e10b9216657fa37e7edff6d8be0709b25bd5066474c229b56cf0098df9",
+                "sha256:26ffc344ec9984d2cd3ca0265007299a8bac8d85c1ad48f4639d8d3aed2af171",
+                "sha256:290e00fa2de74a10688d1bac98d5a9cdd43f14f58e562c580b5b3dfbd358ecae",
+                "sha256:33b15aae79e9147aebe1d0e54099cbcde8d65e3e227cd5b59e49b1272aa0e09d",
+                "sha256:36761693efbe53df179627a775476dcbc37727d6e920958277a7efbc18f1fb73",
+                "sha256:377865821fe80ad984d003723d6f8890bd54ceeb5981b43c0313b9df95411b30",
+                "sha256:49bc857635f935fa30e92e61ac1e87496df8f260a6945a3235e43a9890426866",
+                "sha256:4a506fc223def428f4329e7e1f9fe1c8c593eab226e7c0942c8d75308ad49950",
+                "sha256:533d7dad466ddb7e3b30af274b630eb7c1a6e4ddf01d1c373a0334dc2152110a",
+                "sha256:5fd2252d1fc364ba0e93dd0b7089f4906b66805cb4e6aca7fa8874ac08649647",
+                "sha256:6fead82f0c4000cf38d53f9c030780d81bfa0220218aee13b90b7701c937d95f",
+                "sha256:7f3350fc0a8407d668b13247861c2acd23f7f5fe7d060a3ad9b0820f5fcbcae0",
+                "sha256:8f55d9c6c84d7d09c79fb85979e97b81ec6071cc776a97eb6b96f8f6ec767323",
+                "sha256:98a240cd4c1bfd568aaa52ec42d263131a2582dab82d74d3d42a0d954cac12be",
+                "sha256:9f6f6ffadd6bfa9b2c5b334805eb4b19ca0a5620433659d8f7fb86692c40a359",
+                "sha256:b685453e538b2b5934c58a644ac3f3b3d0cec1a01b6fb26d57388e9f9b674ad0",
+                "sha256:b7865de06779b14d98068da387333ad9bf2756b5b579cc887fac169bc08f87c3",
+                "sha256:bacaa1c16810dd2b3e4417f8e730971b7c4d53d234de61fe4a918db78e80e1e4",
+                "sha256:bae730580ae928de409d63cbe4fdca4c82c3ad2bed30511d19d34e995d63c77e",
+                "sha256:c3389b63222b1c7a158bb7fe69d11ca00066740ec5574596d47a2fe5317f563a",
+                "sha256:c873ba9fd4fa875ad62ce0e4891725e257a8fe7f5abdbc17e51a5d54819be55c",
+                "sha256:db03e3f2c716205fbdabb34d0848459840585225eb97b4f08998c743821ca323",
+                "sha256:f47f757d1b76f0ecb8aa0508ec8d1b390df67a8b67ee2515dc1b046f3a1596ea",
+                "sha256:f891b17e3a3e974e93f9e34e7cca9f530806543571ce078998676a555837d91d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.8.0"
+        },
+        "html2text": {
+            "hashes": [
+                "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b",
+                "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==2020.1.16"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.4"
+        },
+        "imageio": {
+            "hashes": [
+                "sha256:70410af62626a4d725b726ab59138e211e222b80ddf8201c7a6561d694c6238e",
+                "sha256:721f238896a9a99a77b73f06f42fc235d477d5d378cdf34dd0bee1e408b4742c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.31.6"
+        },
+        "imageio-ffmpeg": {
+            "hashes": [
+                "sha256:24095e882a126a0d217197b86265f821b4bb3cf9004104f67c1384a2b4b49168",
+                "sha256:2996c64af3e5489227096580269317719ea1a8121d207f2e28d6c24ebc4a253e",
+                "sha256:39bcd1660118ef360fa4047456501071364661aa9d9021d3d26c58f1ee2081f5",
+                "sha256:7e900c695c6541b1cb17feb1baacd4009b30a53a45b81c23d53a67ab13ffb766",
+                "sha256:7eead662d2f46d748c0ab446b68f423eb63d2b54d0a8ef96f80607245540866d",
+                "sha256:b6de1e18911687c538d5585d8287ab1a23624ca9dc2044fcc4607de667bcf11e"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==0.4.9"
+        },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb",
+                "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==6.8.0"
+        },
+        "itsdangerous": {
+            "hashes": [
+                "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
+                "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "javascripthon": {
+            "hashes": [
+                "sha256:51fbdd0db51cf5d7cd0540f585ad34fb71ad6299e36014f78ba69a09572b964e",
+                "sha256:ec00ba71991043069e436ae200b58a0e2532fba30fa456d6b4b51ff7fc5b797d"
+            ],
+            "version": "==0.12"
+        },
+        "jax": {
+            "hashes": [
+                "sha256:08116481f7336db16c24812bfb5e6f9786915f4c2f6ff4028331fa69e7535202"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.4.8"
+        },
+        "jedi": {
+            "hashes": [
+                "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd",
+                "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.19.1"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
+                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
+            ],
+            "index": "pypi",
+            "version": "==3.1.2"
+        },
+        "joblib": {
+            "hashes": [
+                "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385",
+                "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.2.0"
+        },
+        "json-tricks": {
+            "hashes": [
+                "sha256:71561eedad7c22dde019e9a38ff8c46ebd91da789e31e2513f627dd2cbbdbf56",
+                "sha256:8ba11cb66a09532945c05c7374a72b857dfc3870b2d145125edd508f4027dff9"
+            ],
+            "version": "==3.17.3"
+        },
+        "jsonschema": {
+            "hashes": [
+                "sha256:c9ff4d7447eed9592c23a12ccee508baf0dd0d59650615e847feb6cdca74f392",
+                "sha256:eee9e502c788e89cb166d4d37f43084e3b64ab405c795c03d343a4dbc2c810fc"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.19.2"
+        },
+        "jsonschema-specifications": {
+            "hashes": [
+                "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1",
+                "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2023.7.1"
+        },
+        "keras": {
+            "hashes": [
+                "sha256:35c39534011e909645fb93515452e98e1a0ce23727b55d4918b9c58b2308c15e"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.12.0"
+        },
+        "kiwisolver": {
+            "hashes": [
+                "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b",
+                "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166",
+                "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c",
+                "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c",
+                "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0",
+                "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4",
+                "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9",
+                "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286",
+                "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767",
+                "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c",
+                "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6",
+                "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b",
+                "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004",
+                "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf",
+                "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494",
+                "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac",
+                "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626",
+                "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766",
+                "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514",
+                "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6",
+                "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f",
+                "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d",
+                "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191",
+                "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d",
+                "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51",
+                "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f",
+                "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8",
+                "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454",
+                "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb",
+                "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da",
+                "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8",
+                "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de",
+                "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a",
+                "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9",
+                "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008",
+                "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3",
+                "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32",
+                "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938",
+                "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1",
+                "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9",
+                "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d",
+                "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824",
+                "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b",
+                "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd",
+                "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2",
+                "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5",
+                "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69",
+                "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3",
+                "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae",
+                "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597",
+                "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e",
+                "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955",
+                "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca",
+                "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a",
+                "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea",
+                "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede",
+                "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4",
+                "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6",
+                "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686",
+                "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408",
+                "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871",
+                "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29",
+                "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750",
+                "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897",
+                "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0",
+                "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2",
+                "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09",
+                "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.4.4"
+        },
+        "libclang": {
+            "hashes": [
+                "sha256:2adce42ae652f312245b8f4eda6f30b4076fb61f7619f2dfd0a0c31dee4c32b9",
+                "sha256:65258a6bb3e7dc31dc9b26f8d42f53c9d3b959643ade291fcd1aef4855303ca6",
+                "sha256:7b6686b67a0daa84b4c614bcc119578329fc4fbb52b919565b7376b507c4793b",
+                "sha256:a043138caaf2cb076ebb060c6281ec95612926645d425c691991fc9df00e8a24",
+                "sha256:af55a4aa86fdfe6b2ec68bc8cfe5fdac6c448d591ca7648be86ca17099b41ca8",
+                "sha256:bf4628fc4da7a1dd06a244f9b8e121c5ec68076a763c59d6b13cbb103acc935b",
+                "sha256:eb59652cb0559c0e71784ff4c8ba24c14644becc907b1446563ecfaa622d523b",
+                "sha256:ee20bf93e3dd330f71fc50cdbf13b92ced0aec8e540be64251db53502a9b33f7"
+            ],
+            "version": "==16.0.0"
+        },
+        "markdown": {
+            "hashes": [
+                "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2",
+                "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.4.3"
+        },
+        "markdown-it-py": {
+            "hashes": [
+                "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
+                "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.0.0"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
+                "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
+                "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
+                "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
+                "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
+                "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
+                "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
+                "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
+                "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
+                "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
+                "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
+                "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
+                "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
+                "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
+                "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
+                "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
+                "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
+                "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
+                "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
+                "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
+                "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
+                "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
+                "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
+                "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
+                "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
+                "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
+                "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
+                "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
+                "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
+                "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
+                "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
+                "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
+                "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
+                "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
+                "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
+                "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
+                "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
+                "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
+                "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
+                "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
+                "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
+                "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
+                "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
+                "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
+                "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
+                "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
+                "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
+                "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
+                "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
+                "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "matplotlib": {
+            "hashes": [
+                "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353",
+                "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1",
+                "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290",
+                "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7",
+                "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba",
+                "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717",
+                "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96",
+                "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136",
+                "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba",
+                "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613",
+                "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc",
+                "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de",
+                "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b",
+                "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500",
+                "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea",
+                "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042",
+                "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa",
+                "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb",
+                "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d",
+                "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61",
+                "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882",
+                "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0",
+                "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b",
+                "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6",
+                "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc",
+                "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332",
+                "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439",
+                "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c",
+                "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1",
+                "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529",
+                "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0",
+                "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb",
+                "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7",
+                "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24",
+                "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7",
+                "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87",
+                "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4",
+                "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556",
+                "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476",
+                "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb",
+                "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.7.1"
+        },
+        "mdurl": {
+            "hashes": [
+                "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
+                "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.1.2"
+        },
+        "mediapipe": {
+            "hashes": [
+                "sha256:3071cc0602e0be7258734236aca23acc9106fd1c16b786bdf7bd6fac5c608486",
+                "sha256:368d28c037f5851af4108acf3e19d8e5f4c1ffa8c30e320c8ea61c90a0245dd1",
+                "sha256:8244a4a5bd0ab70d1976359f1b1c59011791bf9b69f63a218745d3cbb9afd700",
+                "sha256:92dd8b27a6dd8063a4c0a44ae939cd4da01751164081649990a92e90068a770d",
+                "sha256:93e3386e05fc8604e92a00a3739631c8e26b6e593acec34a046db6ee5a8704d8",
+                "sha256:9de374683a94d6e60caf47bb0b37e9adf232a1d2936a6edd5a0c32f65453b2a9",
+                "sha256:ca41f298aa6874a86cca60d43d17afb345a4dcdaef50c272d28248c0b1d7de4c",
+                "sha256:e72b93ae242223deacc768e64d4d60171d40e4d1584e4120dfeafc2ba085f8d5",
+                "sha256:e94f1cb9181854672bbe6ba95bfc13f51ace94ff06877045e191befa6f5473c4",
+                "sha256:f0b4a04dc70d0370fef6c05d3861d5830fdc85d2c0c065b17613702e0891bdd1",
+                "sha256:fca06cf26872633ea0792233bfda375c9516248741f3f8afdbae4b556ba0b8c5"
+            ],
+            "index": "pypi",
+            "version": "==0.9.2.1"
+        },
+        "ml-dtypes": {
+            "hashes": [
+                "sha256:273c306db846005b83a98c9c7ec3dc8fa20e8f11c3772c8e8c20cc12d8abfd4b",
+                "sha256:2de6c81b0da398d54aabdd7de599f2dfc43e30b65d9fad379a69f4cc4ae165d3",
+                "sha256:36e8518c8fd2c38729f020125f39ef07b045f5c16d0846320c7252d7773285ee",
+                "sha256:377f2d5cfbf809b59188e0bfda4a0774e658541f575b637fee4850d99c2f9fdc",
+                "sha256:41b6beeaea47e2466b94068664c9a45b2a65dd023aa4e5deeb5a73303661344e",
+                "sha256:77970beeb3cf6ac559c4b6b393f24778a5abd34fafbaad82d5a0d17d0f148936",
+                "sha256:87aa1cf83d41fed5a40fc27ee57ac4c1bf904e940f082531d3d58f1c318b5928",
+                "sha256:8c5c9fe086756fbc1bf51296431d64429536093cf6e2ba592e042d7fc07c8514",
+                "sha256:8de9bbf5bed587a1166699447ea14d1e8fe66d4e812811e37bf2f4d988475476",
+                "sha256:99fab8262d175c49bf1655c229244f301274e8289449c350ba4d5b95ade07d9a",
+                "sha256:a29fbf128583673eca0f43def1dbe77e02c1e8b8a8331db2877bbb57d091ef11",
+                "sha256:ad765159ac6c18d5ee7d325fcf34d3106a9d9d7a49713d998f5cfa330a1459b4",
+                "sha256:b9c5578dffd85637a7dd437192de18bc1a14eb6ba7d53ef40de3f84c51c789e5",
+                "sha256:c1fc0afe63ce99069f9d7e0693a61cfd0aea90241fc3821af9953d0c11f4048a",
+                "sha256:c9218175b06764b8ddc95cb18d11a6c4b48a4b103a31c9ea2b2c3cd0cfc369f8",
+                "sha256:dee8ea629b8e3e20c6649852c1b9deacfa13384ab9337f2c9e717e401d102f23",
+                "sha256:ffb7882dd46399217dc54f37affc899e0a29a4cfb63e5bf733ac0baf4a179c77"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.1.0"
+        },
+        "mne": {
+            "hashes": [
+                "sha256:0d0626d3187dd0ee6f8740d054660a1b5fce4c879f814b745b13c5a587baf32b",
+                "sha256:805bf8e9e99f3fc5e11d504c8f200963cc489b6614cdd3e93227f818f7fdcbe8"
+            ],
+            "index": "pypi",
+            "version": "==1.3.1"
+        },
+        "mne-connectivity": {
+            "hashes": [
+                "sha256:64d398c4332aae1ba246dde967fb71df322d6d9b75db033b13dbf1e5852dfc34",
+                "sha256:7cbdaf0d4ac59fab4fc193f52125995b2f930701f153cf9dcb740d414667c6e8"
+            ],
+            "index": "pypi",
+            "version": "==0.5.0"
+        },
+        "msgpack": {
+            "hashes": [
+                "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862",
+                "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d",
+                "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3",
+                "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672",
+                "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0",
+                "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9",
+                "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee",
+                "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46",
+                "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524",
+                "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819",
+                "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc",
+                "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc",
+                "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1",
+                "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82",
+                "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81",
+                "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6",
+                "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d",
+                "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2",
+                "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c",
+                "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87",
+                "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84",
+                "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e",
+                "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95",
+                "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f",
+                "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b",
+                "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93",
+                "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf",
+                "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61",
+                "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c",
+                "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8",
+                "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d",
+                "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c",
+                "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4",
+                "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba",
+                "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415",
+                "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee",
+                "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d",
+                "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9",
+                "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075",
+                "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f",
+                "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7",
+                "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681",
+                "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329",
+                "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1",
+                "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf",
+                "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c",
+                "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5",
+                "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b",
+                "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5",
+                "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e",
+                "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b",
+                "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad",
+                "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd",
+                "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7",
+                "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002",
+                "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.0.7"
+        },
+        "msgpack-numpy": {
+            "hashes": [
+                "sha256:773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da",
+                "sha256:c667d3180513422f9c7545be5eec5d296dcbb357e06f72ed39cc683797556e69"
+            ],
+            "version": "==0.4.8"
+        },
+        "mtcnn": {
+            "hashes": [
+                "sha256:d0957274584be62cb83d4a089041f8ee3cf3b1893e45f01ed3356f94a381302b",
+                "sha256:fd69d2f4dd10647dd7481a53b9586e805f35a17c61ac78ba472a7f53766eb86e"
+            ],
+            "version": "==0.1.1"
+        },
+        "ndindex": {
+            "hashes": [
+                "sha256:4c0555d352ac9947b0f022562aea9f5d57fa06743ea069669138f75a88b42884",
+                "sha256:bf9bd0b76eeada1c8275e04091f8291869ed2b373b7af48e56faf7579fd2efd2"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.7"
+        },
+        "netcdf4": {
+            "hashes": [
+                "sha256:1194e88dbf5b35dc344a1e520c19140a0a51cc328c32f35d04c5a17ffc623614",
+                "sha256:1831f99bb84d8db5901a4ecb7e382bb8d93847269a48a56b3b3e18cd2b564fd8",
+                "sha256:269f3817604bfbd08c7c8acccb6a5863a0a0222425203d9548ad42cbb554254b",
+                "sha256:3d6a4f7da760ee713aa1197e073c9f066476699dc0cd277428bd5256ebb3878a",
+                "sha256:4f7735871ffc0c8fef710a6b8c5f5af04ed480fee2dc23e737a378f9630d9475",
+                "sha256:5de65543a325451c1ea23b1361fef19da42f7e9874a92a4475c290a045db90b4",
+                "sha256:628d48a31e4b094e807252423e5d1c5b39d4d173fde5cdbe7a18da7626cb606c",
+                "sha256:6fc24dc9d39fee9206710f660b12eb3529d6817583a9de8391e62d4f9f4367fb",
+                "sha256:729544be6ca6a4507d4b3fdd46af578d17c834e1bb53719bebb90ac108035f6a",
+                "sha256:85384e575ddc7e329ca409d380a2d92bb52da5917a171055cf49435f0b6ce07e",
+                "sha256:8c98a3a8cda06920ee8bd24a71226ddf0328c22bd838b0afca9cb45fb4580d99",
+                "sha256:969470f70c6fb51f9fe851cc55d29769c798aeb2de2cb97200f8dac498f299b4",
+                "sha256:9dfab2b84b3f29902515897c6b2b5a92327a78abbfde6ac3917dae80dd17835b",
+                "sha256:a006a912ca204f74f7ec625b1b8d8b06e2f8366fd5be46bacb5e20760684a852",
+                "sha256:a02a8cd53311a447e0c5c185a0d79c3b5d57d49cb1743459417b5e8ca18561bb",
+                "sha256:b46e8404e3526047070f88ad3a65acb813ffccf41d8ff12ff823320be730ba66",
+                "sha256:bbfc767980f87c184f6d6fc9d5a164caa0895c0a6b1820c779f7ec7789c01b0e",
+                "sha256:c94f95ac1ff5590aeb1793eee10519422a9a02da0ece1daf7efa597eabb4e246",
+                "sha256:cb1647d2878a081b4a83fe6d5d5792d8befa59b2b18487bee93aae4b8efa0762",
+                "sha256:ccb1524eb4ea9ec1c4360070b12840784c4afa5e539d509b1f2a921f26e49f39",
+                "sha256:ce9c0b86f603fdaf4723fb92f728265f456f9090544cd62abe27794046bee507",
+                "sha256:d7c45f7f729cfdca8cb7b80373085036a08c88bd6f9d8bfcea559056206f3c3d",
+                "sha256:ddd889dff168baa2fe4777e9117175ecd127e224304950786499744c8956d877",
+                "sha256:e62554a197a344858a526c0cc8340b56d4ab7708feab08528bb602150b8139b1",
+                "sha256:eb06c09bd5a6b44d65d38f544b62f503ede05dacdc9d5cfa6da25b6f738da1bb",
+                "sha256:f6b6d88480052efb3e8b0dd56f773064ed00edcf6ab028ef8fe79a9a06179f43",
+                "sha256:f94a89db78f34fdf68342840efb064fe1474310e8359dffce42e90a9ddf88f2f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.6.3"
+        },
+        "numexpr": {
+            "hashes": [
+                "sha256:0983052f308ea75dd232eb7f4729eed839db8fe8d82289940342b32cc55b15d0",
+                "sha256:11121b14ee3179bade92e823f25f1b94e18716d33845db5081973331188c3338",
+                "sha256:32934d51b5bc8a6636436326da79ed380e2f151989968789cf65b1210572cb46",
+                "sha256:3a84284e0a407ca52980fd20962e89aff671c84cd6e73458f2e29ea2aa206356",
+                "sha256:47a249cecd1382d482a5bf1fac0d11392fb2ed0f7d415ebc4cd901959deb1ec9",
+                "sha256:4ecaa5be24cf8fa0f00108e9dfa1021b7510e9dd9d159b8d8bc7c7ddbb995b31",
+                "sha256:5340d2c86d83f52e1a3e7fd97c37d358ae99af9de316bdeeab2565b9b1e622ca",
+                "sha256:5496fc9e3ae214637cbca1ab556b0e602bd3afe9ff4c943a29c482430972cda8",
+                "sha256:56ec95f8d1db0819e64987dcf1789acd500fa4ea396eeabe4af6efdcb8902d07",
+                "sha256:596eeb3bbfebc912f4b6eaaf842b61ba722cebdb8bc42dfefa657d3a74953849",
+                "sha256:81451962d4145a46dba189df65df101d4d1caddb6efe6ebfe05982cd9f62b2cf",
+                "sha256:8bf005acd7f1985c71b1b247aaac8950d6ea05a0fe0bbbbf3f96cd398b136daa",
+                "sha256:a371cfc1670a18eea2d5c70abaa95a0e8824b70d28da884bad11931266e3a0ca",
+                "sha256:a4546416004ff2e7eb9cf52c2d7ab82732b1b505593193ee9f93fa770edc5230",
+                "sha256:b8a5b2c21c26b62875bf819d375d798b96a32644e3c28bd4ce7789ed1fb489da",
+                "sha256:c7bf60fc1a9c90a9cb21c4c235723e579bff70c8d5362228cb2cf34426104ba2",
+                "sha256:cb2f473fdfd09d17db3038e34818d05b6bc561a36785aa927d6c0e06bccc9911",
+                "sha256:cf5f112bce5c5966c47cc33700bc14ce745c8351d437ed57a9574fff581f341a",
+                "sha256:d43f1f0253a6f2db2f76214e6f7ae9611b422cba3f7d4c86415d7a78bbbd606f",
+                "sha256:d46c47e361fa60966a3339cb4f463ae6151ce7d78ed38075f06e8585d2c8929f",
+                "sha256:d88531ffea3ea9287e8a1665c6a2d0206d3f4660d5244423e2a134a7f0ce5fba",
+                "sha256:da55ba845b847cc33c4bf81cee4b1bddfb0831118cabff8db62888ab8697ec34",
+                "sha256:db1065ba663a854115cf1f493afd7206e2efcef6643129e8061e97a51ad66ebb",
+                "sha256:dccf572763517db6562fb7b17db46aacbbf62a9ca0a66672872f4f71aee7b186",
+                "sha256:e838289e3b7bbe100b99e35496e6cc4cc0541c2207078941ee5a1d46e6b925ae",
+                "sha256:f021ac93cb3dd5d8ba2882627b615b1f58cb089dcc85764c6fbe7a549ed21b0c",
+                "sha256:f29f4d08d9b0ed6fa5d32082971294b2f9131b8577c2b7c36432ed670924313f",
+                "sha256:f3bdf8cbc00c77a46230c765d242f92d35905c239b20c256c48dbac91e49f253",
+                "sha256:fd93b88d5332069916fa00829ea1b972b7e73abcb1081eee5c905a514b8b59e3"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==2.8.7"
+        },
+        "numpy": {
+            "hashes": [
+                "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d",
+                "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07",
+                "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df",
+                "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9",
+                "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d",
+                "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a",
+                "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719",
+                "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2",
+                "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280",
+                "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa",
+                "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387",
+                "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1",
+                "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43",
+                "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f",
+                "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398",
+                "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63",
+                "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de",
+                "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8",
+                "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481",
+                "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0",
+                "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d",
+                "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e",
+                "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96",
+                "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb",
+                "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6",
+                "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d",
+                "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a",
+                "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"
+            ],
+            "index": "pypi",
+            "version": "==1.23.5"
+        },
+        "oauthlib": {
+            "hashes": [
+                "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca",
+                "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.2.2"
+        },
+        "opencv-contrib-python": {
+            "hashes": [
+                "sha256:641ca83b34a9d3e8ef2da70533c6e4e3f076ffb0db69b963d82899cc53e9b3c2",
+                "sha256:698c6b6203831f6573e04258be197e3bfde97fb7279fb614e39d75a8bd5818fb",
+                "sha256:8cad628ea6cc493f6c56140d7edc86f7ed8de528e18e44311e42b390a7d9996e",
+                "sha256:ab33fa2385ec7e70b9d484293f6f1f3707933045af4d18bb3b0a0290fa44370f",
+                "sha256:b54c2e8bb636e367d29bde48fae2aa52c43b782265cf65838a1fe852006cdd94",
+                "sha256:d1fef5ae16dfa73022749165e029e85eb0f399503470c0df1f84c95633f4ae52",
+                "sha256:fefc5f7f1eef3125f78242afe5c989057b36c2f015619698c741b04f4503f913"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==4.7.0.72"
+        },
+        "opencv-python": {
+            "hashes": [
+                "sha256:3424794a711f33284581f3c1e4b071cfc827d02b99d6fd9a35391f517c453306",
+                "sha256:7a297e7651e22eb17c265ddbbc80e2ba2a8ff4f4a1696a67c45e5f5798245842",
+                "sha256:812af57553ec1c6709060c63f6b7e9ad07ddc0f592f3ccc6d00c71e0fe0e6376",
+                "sha256:cd08343654c6b88c5a8c25bf425f8025aed2e3189b4d7306b5861d32affaf737",
+                "sha256:d4f8880440c433a0025d78804dda6901d1e8e541a561dda66892d90290aef881",
+                "sha256:ebfc0a3a2f57716e709028b992e4de7fd8752105d7a768531c4f434043c6f9ff",
+                "sha256:eda115797b114fc16ca6f182b91c5d984f0015c19bec3145e55d33d708e9bae1"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==4.7.0.72"
+        },
+        "openpyxl": {
+            "hashes": [
+                "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184",
+                "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.1.2"
+        },
+        "opt-einsum": {
+            "hashes": [
+                "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147",
+                "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.3.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
+                "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1"
+        },
+        "pandas": {
+            "hashes": [
+                "sha256:0778ab54c8f399d83d98ffb674d11ec716449956bc6f6821891ab835848687f2",
+                "sha256:24472cfc7ced511ac90608728b88312be56edc8f19b9ed885a7d2e47ffaf69c0",
+                "sha256:2d1d138848dd71b37e3cbe7cd952ff84e2ab04d8988972166e18567dcc811245",
+                "sha256:3bb9d840bf15656805f6a3d87eea9dcb7efdf1314a82adcf7f00b820427c5570",
+                "sha256:425705cee8be54db2504e8dd2a730684790b15e5904b750c367611ede49098ab",
+                "sha256:4f3320bb55f34af4193020158ef8118ee0fb9aec7cc47d2084dbfdd868a0a24f",
+                "sha256:4ffb14f50c74ee541610668137830bb93e9dfa319b1bef2cedf2814cd5ac9c70",
+                "sha256:52c858de9e9fc422d25e67e1592a6e6135d7bcf9a19fcaf4d0831a0be496bf21",
+                "sha256:57c34b79c13249505e850d0377b722961b99140f81dafbe6f19ef10239f6284a",
+                "sha256:6ded51f7e3dd9b4f8b87f2ceb7bd1a8df2491f7ee72f7074c6927a512607199e",
+                "sha256:70db5c278bbec0306d32bf78751ff56b9594c05a5098386f6c8a563659124f91",
+                "sha256:78425ca12314b23356c28b16765639db10ebb7d8983f705d6759ff7fe41357fa",
+                "sha256:8318de0f886e4dcb8f9f36e45a3d6a6c3d1cfdc508354da85e739090f0222991",
+                "sha256:8f987ec26e96a8490909bc5d98c514147236e49830cba7df8690f6087c12bbae",
+                "sha256:9253edfd015520ce77a9343eb7097429479c039cd3ebe81d7810ea11b4b24695",
+                "sha256:977326039bd1ded620001a1889e2ed4798460a6bc5a24fbaebb5f07a41c32a55",
+                "sha256:a4f789b7c012a608c08cda4ff0872fd979cb18907a37982abe884e6f529b8793",
+                "sha256:b3ba8f5dd470d8bfbc4259829589f4a32881151c49e36384d9eb982b35a12020",
+                "sha256:b5337c87c4e963f97becb1217965b6b75c6fe5f54c4cf09b9a5ac52fc0bd03d3",
+                "sha256:bbb2c5e94d6aa4e632646a3bacd05c2a871c3aa3e85c9bec9be99cb1267279f2",
+                "sha256:c24c7d12d033a372a9daf9ff2c80f8b0af6f98d14664dbb0a4f6a029094928a7",
+                "sha256:cda9789e61b44463c1c4fe17ef755de77bcd13b09ba31c940d20f193d63a5dc8",
+                "sha256:d08e41d96bc4de6f500afe80936c68fce6099d5a434e2af7c7fd8e7c72a3265d",
+                "sha256:d93b7fcfd9f3328072b250d6d001dcfeec5d3bb66c1b9c8941e109a46c0c01a8",
+                "sha256:fcd471c9d9f60926ab2f15c6c29164112f458acb42280365fbefa542d0c2fc74"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.0.0"
+        },
+        "parso": {
+            "hashes": [
+                "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0",
+                "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.8.3"
+        },
+        "pillow": {
+            "hashes": [
+                "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1",
+                "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba",
+                "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a",
+                "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799",
+                "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51",
+                "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb",
+                "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5",
+                "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270",
+                "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6",
+                "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47",
+                "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf",
+                "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e",
+                "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b",
+                "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66",
+                "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865",
+                "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec",
+                "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c",
+                "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1",
+                "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38",
+                "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906",
+                "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705",
+                "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef",
+                "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc",
+                "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f",
+                "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf",
+                "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392",
+                "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d",
+                "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe",
+                "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32",
+                "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5",
+                "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7",
+                "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44",
+                "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d",
+                "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3",
+                "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625",
+                "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e",
+                "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829",
+                "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089",
+                "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3",
+                "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78",
+                "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96",
+                "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964",
+                "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597",
+                "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99",
+                "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a",
+                "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140",
+                "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7",
+                "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16",
+                "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903",
+                "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1",
+                "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296",
+                "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572",
+                "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115",
+                "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a",
+                "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd",
+                "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4",
+                "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1",
+                "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb",
+                "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa",
+                "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a",
+                "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569",
+                "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c",
+                "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf",
+                "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082",
+                "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062",
+                "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==9.5.0"
+        },
+        "platformdirs": {
+            "hashes": [
+                "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08",
+                "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.2.0"
+        },
+        "pooch": {
+            "hashes": [
+                "sha256:74258224fc33d58f53113cf955e8d51bf01386b91492927d0d1b6b341a765ad7",
+                "sha256:f174a1041b6447f0eef8860f76d17f60ed2f857dc0efa387a7f08228af05d998"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.7.0"
+        },
+        "protobuf": {
+            "hashes": [
+                "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
+                "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
+                "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
+                "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
+                "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
+                "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
+                "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
+                "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
+                "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
+                "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
+                "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
+                "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
+                "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
+                "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
+                "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
+                "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
+                "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
+                "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
+                "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
+                "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
+                "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
+                "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.20.3"
+        },
+        "psutil": {
+            "hashes": [
+                "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff",
+                "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1",
+                "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62",
+                "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549",
+                "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08",
+                "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7",
+                "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e",
+                "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe",
+                "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24",
+                "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad",
+                "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94",
+                "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8",
+                "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7",
+                "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==5.9.4"
+        },
+        "psychopy": {
+            "hashes": [
+                "sha256:f856549eb9099a062c59d53f4abee0864e780fd5c8027f1802412444b47ac355"
+            ],
+            "index": "pypi",
+            "version": "==2023.2.3"
+        },
+        "psychtoolbox": {
+            "hashes": [
+                "sha256:0c5c53b0756e4790b71ec1dec2dccf5165c15571b8f8e72ca8f5a51b01417292",
+                "sha256:6a1fc1daff69d71c36795b4e7544be2534fc00b7732f5466ddcb6f15df3007a0",
+                "sha256:72735d5b659097a92a2631ff5a32c8d05bccdb961223d835919f8c9a9fed5290",
+                "sha256:774239046c967db9e5c70fe84345601d53e51fc0e06af0bdf9d8d6a4b7bdd696",
+                "sha256:8aeed0d44e7476b0656e867738d32243e2c4f0bbcb42c549711fb33fefa6e288",
+                "sha256:a8022f70715574f23ea1c760eceb7db6c5ac36f9fb96d4b729fea0793a66c388",
+                "sha256:c9b0de05a2c02a8778756c1084a673bfb6ab1add7188556d1c232e686c75ec9d",
+                "sha256:f75974b295d69f1e4fc08ef1a8f62ae7e5c7f46943493a24175514c8c8c4190f",
+                "sha256:fdf0b93711284bddf7aece840898dbcd8a83be8196e6a37046503a95c63e6c59"
+            ],
+            "version": "==3.0.19.0"
+        },
+        "py-cpuinfo": {
+            "hashes": [
+                "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690",
+                "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"
+            ],
+            "version": "==9.0.0"
+        },
+        "py-iir-filter": {
+            "hashes": [
+                "sha256:f2c245c7fbdfe0945e76cadf0e2f1f80005a9b35a16b92ef4adf02e56fff9acb"
+            ],
+            "index": "pypi",
+            "version": "==1.1.0"
+        },
+        "pyarrow": {
+            "hashes": [
+                "sha256:0f4054e5ee6c88ca256a67fc8b27f9c59bcd385216346265831d462a6069033f",
+                "sha256:1541e9209c094e7f4d7b43fdd9de3a8c71d3069cf6fc03b59bf5774042411849",
+                "sha256:28de7c05b4d7a71ec660360639cc9b65ceb1175e0e9d4dfccd879a1545bc38f7",
+                "sha256:2fbb7ab62537782c5ab31aa08db0e1f6de92c2c515fdfc0790128384e919adcb",
+                "sha256:35abf61bd0cc9daca3afc715f6ba74ea83d792fa040025352624204bec66bf6a",
+                "sha256:378955365dd087c285ef4f34ad939d7e551b7715326710e8cd21cfa2ce511bd7",
+                "sha256:3eccce331a1392e46573f2ce849a9ee3c074e0d7008e9be0b44566ac149fd6a1",
+                "sha256:42509e6c93b4a1c8ae8ccd939a43f437097783fe130a1991497a6a1abbba026f",
+                "sha256:426ffec63ab9b4dff23dec51be2150e3a4a99eb38e66c10a70e2c48779fe9c9d",
+                "sha256:4362ed90def81640addcd521811dd16a13015f0a8255bec324a41262c1524b6c",
+                "sha256:45d3324e1c9871a07de6b4d514ebd73225490963a6dd46c64c465c4b6079fe1e",
+                "sha256:466c1a5a7a4b279cfa363ac34dedd0c3c6af388cec9e6a468ffc095a6627849a",
+                "sha256:4fce1db17efbc453080c5b306f021926de7c636456a128328797e574c151f81a",
+                "sha256:5b2b8f87951b08a3e72265c8963da3fe4f737bb81290269037e047dd172aa591",
+                "sha256:65c377523b369f7ef1ba02be814e832443bb3b15065010838f02dae5bdc0f53c",
+                "sha256:6867f6a8057eaef5a7ac6d27fe5518133f67973c5d4295d79a943458350e7c61",
+                "sha256:687d0df1e08876b2d24d42abae129742fc655367e3fe6700aa4d79fcf2e3215e",
+                "sha256:6c94056fb5f0ee0bae2206c3f776881e1db2bd0d133d06805755ae7ac5145349",
+                "sha256:768b962e4c042ab2c96576ca0757935472e220d11af855c7d0be3279d7fced5f",
+                "sha256:771079fddc0b4440c41af541dbdebc711a7062c93d3c4764476a9442606977db",
+                "sha256:77293b1319c7044f68ebfa43db8c929a0a5254ce371f1a0873d343f1460171d0",
+                "sha256:80225768d94024d59a31320374f5e6abf8899866c958dfb4f4ea8e2d9ec91bde",
+                "sha256:8c05e6c45d303c80e41ab04996430a0251321f70986ed51213903ea7bc0b7efd",
+                "sha256:968844f591902160bd3c9ee240ce8822a3b4e7de731e91daea76ad43fe0ff062",
+                "sha256:97993a12aacc781efad9c92d4545a877e803c4d106d34237ec4ce987bec825a3",
+                "sha256:a1c9675966662a042caebbaafa1ae7fc26291287ebc3da06aa63ad74c323ec30",
+                "sha256:ad7095f8f0fe0bfa3d3fca1909b8fa15c70e630b0cc1ff8d35e143f5e2704064",
+                "sha256:b61546977a8bd7e3d0c697ede723341ef4737e761af2239aef6e1db447f97727",
+                "sha256:c4096136318de1c4937370c0c365f949961c371201c396d8cc94a353f342069d",
+                "sha256:ca54b87c46abdfe027f18f959ca388102bd7326c344838f72244807462d091b2",
+                "sha256:d2bc7c53941d85f0133b1bd5a814bca0af213922f50d8a8dc0eed4d9ed477845",
+                "sha256:dcedbc0b4ea955c530145acfe99e324875c386419a09db150291a24cb01aeb81",
+                "sha256:e6602272fce71c0fb64f266e7cdbe51b93b00c22fc1bb57f2b0cb681c4aeedf4",
+                "sha256:e8a1e470e4b5f7bda7bede0410291daec55ab69f346d77795d34fd6a45b41579",
+                "sha256:ecc463c45f2b6b36431f5f2025842245e8c15afe4d42072230575785f3bb00c6",
+                "sha256:f05e81b4c621e6ad4bcd8f785e3aa1d6c49a935818b809ea6e7bf206a5b1a4e8"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==14.0.0"
+        },
+        "pyasn1": {
+            "hashes": [
+                "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
+                "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
+            ],
+            "version": "==0.4.8"
+        },
+        "pyasn1-modules": {
+            "hashes": [
+                "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
+                "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
+            ],
+            "version": "==0.2.8"
+        },
+        "pycparser": {
+            "hashes": [
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+            ],
+            "version": "==2.21"
+        },
+        "pydantic": {
+            "hashes": [
+                "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e",
+                "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6",
+                "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd",
+                "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca",
+                "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b",
+                "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a",
+                "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245",
+                "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d",
+                "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee",
+                "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1",
+                "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3",
+                "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d",
+                "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5",
+                "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914",
+                "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd",
+                "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1",
+                "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e",
+                "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e",
+                "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a",
+                "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd",
+                "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f",
+                "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209",
+                "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d",
+                "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a",
+                "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143",
+                "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918",
+                "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52",
+                "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e",
+                "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f",
+                "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e",
+                "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb",
+                "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe",
+                "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe",
+                "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d",
+                "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209",
+                "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.10.7"
+        },
+        "pydantic-core": {
+            "hashes": [
+                "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e",
+                "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33",
+                "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7",
+                "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7",
+                "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea",
+                "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4",
+                "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0",
+                "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7",
+                "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94",
+                "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff",
+                "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82",
+                "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd",
+                "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893",
+                "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e",
+                "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d",
+                "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901",
+                "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9",
+                "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c",
+                "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7",
+                "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891",
+                "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f",
+                "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a",
+                "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9",
+                "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5",
+                "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e",
+                "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a",
+                "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c",
+                "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f",
+                "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514",
+                "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b",
+                "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302",
+                "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096",
+                "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0",
+                "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27",
+                "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884",
+                "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a",
+                "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357",
+                "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430",
+                "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221",
+                "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325",
+                "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4",
+                "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05",
+                "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55",
+                "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875",
+                "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970",
+                "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc",
+                "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6",
+                "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f",
+                "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b",
+                "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d",
+                "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15",
+                "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118",
+                "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee",
+                "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e",
+                "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6",
+                "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208",
+                "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede",
+                "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3",
+                "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e",
+                "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada",
+                "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175",
+                "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a",
+                "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c",
+                "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f",
+                "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58",
+                "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f",
+                "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a",
+                "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a",
+                "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921",
+                "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e",
+                "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904",
+                "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776",
+                "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52",
+                "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf",
+                "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8",
+                "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f",
+                "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b",
+                "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63",
+                "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c",
+                "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f",
+                "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468",
+                "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e",
+                "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab",
+                "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2",
+                "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb",
+                "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb",
+                "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132",
+                "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b",
+                "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607",
+                "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934",
+                "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698",
+                "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e",
+                "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561",
+                "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de",
+                "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b",
+                "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a",
+                "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595",
+                "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402",
+                "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881",
+                "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429",
+                "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5",
+                "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7",
+                "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c",
+                "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531",
+                "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6",
+                "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.10.1"
+        },
+        "pydeck": {
+            "hashes": [
+                "sha256:9e0a67890ab061b8c6080e06f8c780934c00355a7114291c884f055f3fc0dc25",
+                "sha256:c89b3dd76f9991140a33b886b336c762105e9c9def8e842e891bc72dbce8a4ce"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.8.1b0"
+        },
+        "pyedflib": {
+            "hashes": [
+                "sha256:027f3f9c2cad6ecaef3a2a44cfdd80f3da81cfac635c53a6bbd30d107ce39ceb",
+                "sha256:0904cd3de347b4755dca52bf810715b99d7bf2e0591c6af011c0fd289a4a36bd",
+                "sha256:1c8f716046f286ebca630e380290629e5f3f4ff697c384ee902b50b0e1a4536c",
+                "sha256:2bd4ae5265772d672283a4511ec2a98de1eaad184004086892673cafe5a3acb6",
+                "sha256:2d65a4eac9b526cf9e7e62a6aaac61a3990b517712205063f22b36f1d9cbe3b1",
+                "sha256:30de5d908e6461ede89b7cd501f9589fcaa0d2578b700c1d04d61bbb8dd1e4cb",
+                "sha256:349a2f3a42477c89dea51e6c299ec4f3094287897c8c1ecbf3ccfcb8ccdac8df",
+                "sha256:3c0c4c2aedeefd6094b9f7f0ba41f9c57b81e728cb212ff38c6c0626651923a0",
+                "sha256:449db6e99dff397bb9d2ccbe06f63c2679c9e8c2d1c2ad536ae57444e1f89767",
+                "sha256:4a441f934b432da6014a4e2693f858596b06328a8d02036591c02842e717043f",
+                "sha256:4fc2a1ef6e4a175255425ff868559551b6b79599407e84ce1bb4012cf2ed952b",
+                "sha256:5e5dd41cf5245062bcd14825abd3a9f1bc1f944a92f66dae6486626667a58661",
+                "sha256:614d07509510da93a613f21feb811c60b65bd77beea30bced8d47a6f3147c39e",
+                "sha256:639af9b08eb50cf786167942fe4d57b29a5e3bf589979d1a49b9a9672f52cb8e",
+                "sha256:78ebde94bf0a5cdd046534362c6fa5822ef0ec03959394797e85dff1e048042c",
+                "sha256:874a664bf947b1f19ed5227e8390d66a579a35b800671c939e271d0e613d5ba5",
+                "sha256:8811c7e2eda24dba80c05faa3af5ca631b42a1110749e158cb215d7676f089da",
+                "sha256:8ab2243f9cee201833161a27865680ef0e419556c5ad42d6b08abee81bffd0d4",
+                "sha256:8c9b1affcb63df9954f4cb51100da6ca3f7bbe092fdb3c826ff9ffe99d1fe707",
+                "sha256:8f2f71316cf4e96cb0887b56e1d64b2ca2a714d90a48dfb0a8632a56a7a197cf",
+                "sha256:905418440280e86c433f4cfb7237666cc389865b7b73291fedc620ed7654c721",
+                "sha256:98c021deefbfd540c3d487c6c6fe634b1b9763141593c6c04c90ab7851215a76",
+                "sha256:9d228b495677709c39ff16c12dd0027ac726133eb4b6234e6c63cee2a4141af0",
+                "sha256:a300d23145b78c004e5c7abfad58e3a99c41c635984470f5cade015b79553bc9",
+                "sha256:ac4ebae1c1d5d4b3f2a9da1873d6fabba8990cb033be032763fab01c3117eb7d",
+                "sha256:acd14a1636b911807c900e9697576a0bd43fc6b0a563208c641f4b20edba6c60",
+                "sha256:acdf079833d7ece3853d7e1e593c01574d1475d7b5dbd5fc8d5d1e3d1051de49",
+                "sha256:b4f9850d6d505e98c3e36084ee884f32e53dabbb4eebcdc8d220f56ac31829e2",
+                "sha256:bc53e30e6c9d6d00c8a3a61b91c50611ed51bc9205d06ea75bbbb37e58492d93",
+                "sha256:bd4284da378832eaf04b538db1da2888543bb05bc84716c02c6bb61eef3db60e",
+                "sha256:c1bf09f35d50164811b87164b723225539ad0242637286368610fc6eaa9a3de6",
+                "sha256:cbc37b3d1feb7946625b8bd6e8597f68790bebc687afa6971f185332bf139518",
+                "sha256:d19fa1b13ae8d489ea47660c8f9d8b389fc585650c485d6806e8191961ec87f3",
+                "sha256:d92308ec93001d645f4a53792076cd6606bf216bb19361fbdf41f0001690db6a",
+                "sha256:da610654e6b4f4bbc953cf218eac93eb20920974b76a77856a92b67801e4552a",
+                "sha256:debff3da3429307c436b95af52bab50dd9338e97501e593d6e34f9e0df9c3cb3",
+                "sha256:dfe285a8c5f6769a55408a5f94776320ca5370f68b7df56a788dec9ca02219d8",
+                "sha256:e58bc095593cba637b62fcd09449ae1eb30523913e3dc76302ba71d8a9c893fe",
+                "sha256:f4f50c4a21ab421c281394993fff811238fcf821c08e936f35f92b9f14832291",
+                "sha256:fa27bdf831ac398f7f93bc697ec9138c3ce3cf910f474f30d8a15612d58a6ae3",
+                "sha256:fd0643ec750195fc27a954c85899d07e44909297ed8b8667d99acc91a6cb4fbe"
+            ],
+            "index": "pypi",
+            "version": "==0.1.30"
+        },
+        "pyglet": {
+            "hashes": [
+                "sha256:8a8317fbb2bae145bd80f6d92d66b6dbbc9d13f1cbbed682ff55793a63003a46",
+                "sha256:e4cc8dc2f09d8487f7b3e2d93bd1961528afe989d058177b26a46d3508fd2c33"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==1.4.11"
+        },
+        "pygments": {
+            "hashes": [
+                "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
+                "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.16.1"
+        },
+        "pyparallel": {
+            "hashes": [
+                "sha256:b5550293af42a42d7b2e1ada1224d3c3ce2f09b80e85421820e068655908c611",
+                "sha256:f382422c97a885453b405acadd27c522bff87e9407968dc814955ed68b1cc777"
+            ],
+            "markers": "platform_system != 'Darwin'",
+            "version": "==0.2.2"
+        },
+        "pyparsing": {
+            "hashes": [
+                "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
+                "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
+            ],
+            "markers": "python_full_version >= '3.6.8'",
+            "version": "==3.0.9"
+        },
+        "pypi-search": {
+            "hashes": [
+                "sha256:7fc7f5fe6be85f13110472493c0b5ec3fc361642774d567c5c591469a1f91880",
+                "sha256:8e64a4faa7d438670e97cec3040369a97433fba7e64b8c3a6d3b8ccdc3824438"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2.0"
+        },
+        "pypiwin32": {
+            "hashes": [
+                "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775",
+                "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==223"
+        },
+        "pyqt5": {
+            "hashes": [
+                "sha256:501355f327e9a2c38db0428e1a236d25ebcb99304cd6e668c05d1188d514adec",
+                "sha256:862cea3be95b4b0a2b9678003b3a18edf7bd5eafd673860f58820f246d4bf616",
+                "sha256:93288d62ebd47b1933d80c27f5d43c7c435307b84d480af689cef2474e87e4c8",
+                "sha256:b89478d16d4118664ff58ed609e0a804d002703c9420118de7e4e70fa1cb5486",
+                "sha256:d46b7804b1b10a4ff91753f8113e5b5580d2b4462f3226288e2d84497334898a",
+                "sha256:ff99b4f91aa8eb60510d5889faad07116d3340041916e46c07d519f7cad344e1"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==5.15.10"
+        },
+        "pyqt5-qt5": {
+            "hashes": [
+                "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a",
+                "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962",
+                "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154",
+                "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"
+            ],
+            "version": "==5.15.2"
+        },
+        "pyqt5-sip": {
+            "hashes": [
+                "sha256:0f85fb633a522f04e48008de49dce1ff1d947011b48885b8428838973fbca412",
+                "sha256:108a15f603e1886988c4b0d9d41cb74c9f9815bf05cefc843d559e8c298a10ce",
+                "sha256:1c8371682f77852256f1f2d38c41e2e684029f43330f0635870895ab01c02f6c",
+                "sha256:205cd449d08a2b024a468fb6100cd7ed03e946b4f49706f508944006f955ae1a",
+                "sha256:29fa9cc964517c9fc3f94f072b9a2aeef4e7a2eda1879cb835d9e06971161cdf",
+                "sha256:3188a06956aef86f604fb0d14421a110fad70d2a9e943dbacbfc3303f651dade",
+                "sha256:3a4498f3b1b15f43f5d12963accdce0fd652b0bcaae6baf8008663365827444c",
+                "sha256:5338773bbaedaa4f16a73c142fb23cc18c327be6c338813af70260b756c7bc92",
+                "sha256:6e4ac714252370ca037c7d609da92388057165edd4f94e63354f6d65c3ed9d53",
+                "sha256:773731b1b5ab1a7cf5621249f2379c95e3d2905e9bd96ff3611b119586daa876",
+                "sha256:7f321daf84b9c9dbca61b80e1ef37bdaffc0e93312edae2cd7da25b953971d91",
+                "sha256:7fe3375b508c5bc657d73b9896bba8a768791f1f426c68053311b046bcebdddf",
+                "sha256:96414c93f3d33963887cf562d50d88b955121fbfd73f937c8eca46643e77bf61",
+                "sha256:9a8cdd6cb66adcbe5c941723ed1544eba05cf19b6c961851b58ccdae1c894afb",
+                "sha256:9b984c2620a7a7eaf049221b09ae50a345317add2624c706c7d2e9e6632a9587",
+                "sha256:a7e3623b2c743753625c4650ec7696362a37fb36433b61824cf257f6d3d43cca",
+                "sha256:bbc7cd498bf19e0862097be1ad2243e824dea56726f00c11cff1b547c2d31d01",
+                "sha256:d5032da3fff62da055104926ffe76fd6044c1221f8ad35bb60804bcb422fe866",
+                "sha256:db228cd737f5cbfc66a3c3e50042140cb80b30b52edc5756dbbaa2346ec73137",
+                "sha256:ec60162e034c42fb99859206d62b83b74f987d58937b3a82bdc07b5c3d190dec",
+                "sha256:fb4a5271fa3f6bc2feb303269a837a95a6d8dd16be553aa40e530de7fb81bfdf"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==12.13.0"
+        },
+        "pyserial": {
+            "hashes": [
+                "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
+                "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
+            ],
+            "index": "pypi",
+            "version": "==3.5"
+        },
+        "pysocks": {
+            "hashes": [
+                "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
+                "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
+                "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
+            ],
+            "version": "==1.7.1"
+        },
+        "python-bidi": {
+            "hashes": [
+                "sha256:50eef6f6a0bbdd685f9e8c207f3c9050f5b578d0a46e37c76a9c4baea2cc2e13",
+                "sha256:5347f71e82b3e9976dc657f09ded2bfe39ba8d6777ca81a5b2c56c30121c496e"
+            ],
+            "version": "==0.4.2"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.8.2"
+        },
+        "python-gitlab": {
+            "hashes": [
+                "sha256:9da309ce97d5de56947bd8ad09e321ab7de21bb360796c6c0e083397d1ff3399",
+                "sha256:e4912083d5d40a79b57875a033083e2c860e34bcc4644ef64ca377a755c5b0c9"
+            ],
+            "markers": "python_full_version >= '3.8.0'",
+            "version": "==4.1.1"
+        },
+        "python-multipart": {
+            "hashes": [
+                "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132",
+                "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"
+            ],
+            "index": "pypi",
+            "version": "==0.0.6"
+        },
+        "python-vlc": {
+            "hashes": [
+                "sha256:508bc5b4b4fd72b4e23c926795bdcd38c7c1c08a4dd6b8cc87b0abd1d7118aa1",
+                "sha256:a4d3bdddfce84a8fb1b2d5447193a0239c55c16ca246e5194d48efd59c4e236b"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==3.0.11115"
+        },
+        "pytz": {
+            "hashes": [
+                "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588",
+                "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"
+            ],
+            "version": "==2023.3"
+        },
+        "pywin32": {
+            "hashes": [
+                "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d",
+                "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65",
+                "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e",
+                "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b",
+                "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4",
+                "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040",
+                "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a",
+                "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36",
+                "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8",
+                "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e",
+                "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802",
+                "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a",
+                "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407",
+                "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"
+            ],
+            "version": "==306"
+        },
+        "pywinhook": {
+            "hashes": [
+                "sha256:14d9bea43fce652e027399bcc800a148fb4e41b2801cfb9347f1b47d0fb59d0e",
+                "sha256:18fe2f63245d8a2f9d83f8d9c385e3695a6363badd50d492eb3e7f6f06a01c0c",
+                "sha256:26e9408ccabe393244f7a164fa82b3cab0c7fc6bf96957a9f35732ec59fedf1c",
+                "sha256:78bc1c3af385fd67e9ddfc7ab5a4f1c599d67347df471d628ece50e12911586b",
+                "sha256:c478bf3142ab63cc0fac83250228269113f25b06f2a9142e5772fbb9429d67b8",
+                "sha256:ee2862dce5af02e54879fa9117e78cb736f8cc31e07a18ae6399ee33f4537b53",
+                "sha256:f3b20103cec8908f72254f1574bb166f29ce6a95fae9520e071b411d9e3e6f21"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==1.6.2"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==6.0.1"
+        },
+        "pyzmq": {
+            "hashes": [
+                "sha256:019e59ef5c5256a2c7378f2fb8560fc2a9ff1d315755204295b2eab96b254d0a",
+                "sha256:034239843541ef7a1aee0c7b2cb7f6aafffb005ede965ae9cbd49d5ff4ff73cf",
+                "sha256:03b3f49b57264909aacd0741892f2aecf2f51fb053e7d8ac6767f6c700832f45",
+                "sha256:047a640f5c9c6ade7b1cc6680a0e28c9dd5a0825135acbd3569cc96ea00b2505",
+                "sha256:04ccbed567171579ec2cebb9c8a3e30801723c575601f9a990ab25bcac6b51e2",
+                "sha256:057e824b2aae50accc0f9a0570998adc021b372478a921506fddd6c02e60308e",
+                "sha256:11baebdd5fc5b475d484195e49bae2dc64b94a5208f7c89954e9e354fc609d8f",
+                "sha256:11c1d2aed9079c6b0c9550a7257a836b4a637feb334904610f06d70eb44c56d2",
+                "sha256:11d58723d44d6ed4dd677c5615b2ffb19d5c426636345567d6af82be4dff8a55",
+                "sha256:12720a53e61c3b99d87262294e2b375c915fea93c31fc2336898c26d7aed34cd",
+                "sha256:17ef5f01d25b67ca8f98120d5fa1d21efe9611604e8eb03a5147360f517dd1e2",
+                "sha256:18d43df3f2302d836f2a56f17e5663e398416e9dd74b205b179065e61f1a6edf",
+                "sha256:1a5d26fe8f32f137e784f768143728438877d69a586ddeaad898558dc971a5ae",
+                "sha256:1af379b33ef33757224da93e9da62e6471cf4a66d10078cf32bae8127d3d0d4a",
+                "sha256:1ccf825981640b8c34ae54231b7ed00271822ea1c6d8ba1090ebd4943759abf5",
+                "sha256:21eb4e609a154a57c520e3d5bfa0d97e49b6872ea057b7c85257b11e78068222",
+                "sha256:2243700cc5548cff20963f0ca92d3e5e436394375ab8a354bbea2b12911b20b0",
+                "sha256:255ca2b219f9e5a3a9ef3081512e1358bd4760ce77828e1028b818ff5610b87b",
+                "sha256:259c22485b71abacdfa8bf79720cd7bcf4b9d128b30ea554f01ae71fdbfdaa23",
+                "sha256:25f0e6b78220aba09815cd1f3a32b9c7cb3e02cb846d1cfc526b6595f6046618",
+                "sha256:273bc3959bcbff3f48606b28229b4721716598d76b5aaea2b4a9d0ab454ec062",
+                "sha256:292fe3fc5ad4a75bc8df0dfaee7d0babe8b1f4ceb596437213821f761b4589f9",
+                "sha256:2ca57a5be0389f2a65e6d3bb2962a971688cbdd30b4c0bd188c99e39c234f414",
+                "sha256:2d163a18819277e49911f7461567bda923461c50b19d169a062536fffe7cd9d2",
+                "sha256:2d81f1ddae3858b8299d1da72dd7d19dd36aab654c19671aa8a7e7fb02f6638a",
+                "sha256:2f957ce63d13c28730f7fd6b72333814221c84ca2421298f66e5143f81c9f91f",
+                "sha256:330f9e188d0d89080cde66dc7470f57d1926ff2fb5576227f14d5be7ab30b9fa",
+                "sha256:34c850ce7976d19ebe7b9d4b9bb8c9dfc7aac336c0958e2651b88cbd46682123",
+                "sha256:35b5ab8c28978fbbb86ea54958cd89f5176ce747c1fb3d87356cf698048a7790",
+                "sha256:3669cf8ee3520c2f13b2e0351c41fea919852b220988d2049249db10046a7afb",
+                "sha256:381469297409c5adf9a0e884c5eb5186ed33137badcbbb0560b86e910a2f1e76",
+                "sha256:3d0a409d3b28607cc427aa5c30a6f1e4452cc44e311f843e05edb28ab5e36da0",
+                "sha256:44e58a0554b21fc662f2712814a746635ed668d0fbc98b7cb9d74cb798d202e6",
+                "sha256:458dea649f2f02a0b244ae6aef8dc29325a2810aa26b07af8374dc2a9faf57e3",
+                "sha256:48e466162a24daf86f6b5ca72444d2bf39a5e58da5f96370078be67c67adc978",
+                "sha256:49d238cf4b69652257db66d0c623cd3e09b5d2e9576b56bc067a396133a00d4a",
+                "sha256:4ca1ed0bb2d850aa8471387882247c68f1e62a4af0ce9c8a1dbe0d2bf69e41fb",
+                "sha256:52533489f28d62eb1258a965f2aba28a82aa747202c8fa5a1c7a43b5db0e85c1",
+                "sha256:548d6482dc8aadbe7e79d1b5806585c8120bafa1ef841167bc9090522b610fa6",
+                "sha256:5619f3f5a4db5dbb572b095ea3cb5cc035335159d9da950830c9c4db2fbb6995",
+                "sha256:57459b68e5cd85b0be8184382cefd91959cafe79ae019e6b1ae6e2ba8a12cda7",
+                "sha256:5a34d2395073ef862b4032343cf0c32a712f3ab49d7ec4f42c9661e0294d106f",
+                "sha256:61706a6b6c24bdece85ff177fec393545a3191eeda35b07aaa1458a027ad1304",
+                "sha256:724c292bb26365659fc434e9567b3f1adbdb5e8d640c936ed901f49e03e5d32e",
+                "sha256:73461eed88a88c866656e08f89299720a38cb4e9d34ae6bf5df6f71102570f2e",
+                "sha256:76705c9325d72a81155bb6ab48d4312e0032bf045fb0754889133200f7a0d849",
+                "sha256:76c1c8efb3ca3a1818b837aea423ff8a07bbf7aafe9f2f6582b61a0458b1a329",
+                "sha256:77a41c26205d2353a4c94d02be51d6cbdf63c06fbc1295ea57dad7e2d3381b71",
+                "sha256:79986f3b4af059777111409ee517da24a529bdbd46da578b33f25580adcff728",
+                "sha256:7cff25c5b315e63b07a36f0c2bab32c58eafbe57d0dce61b614ef4c76058c115",
+                "sha256:7f7e58effd14b641c5e4dec8c7dab02fb67a13df90329e61c869b9cc607ef752",
+                "sha256:820c4a08195a681252f46926de10e29b6bbf3e17b30037bd4250d72dd3ddaab8",
+                "sha256:87e34f31ca8f168c56d6fbf99692cc8d3b445abb5bfd08c229ae992d7547a92a",
+                "sha256:8f03d3f0d01cb5a018debeb412441996a517b11c5c17ab2001aa0597c6d6882c",
+                "sha256:90f26dc6d5f241ba358bef79be9ce06de58d477ca8485e3291675436d3827cf8",
+                "sha256:955215ed0604dac5b01907424dfa28b40f2b2292d6493445dd34d0dfa72586a8",
+                "sha256:985bbb1316192b98f32e25e7b9958088431d853ac63aca1d2c236f40afb17c83",
+                "sha256:a382372898a07479bd34bda781008e4a954ed8750f17891e794521c3e21c2e1c",
+                "sha256:a882ac0a351288dd18ecae3326b8a49d10c61a68b01419f3a0b9a306190baf69",
+                "sha256:aa8d6cdc8b8aa19ceb319aaa2b660cdaccc533ec477eeb1309e2a291eaacc43a",
+                "sha256:abc719161780932c4e11aaebb203be3d6acc6b38d2f26c0f523b5b59d2fc1996",
+                "sha256:abf34e43c531bbb510ae7e8f5b2b1f2a8ab93219510e2b287a944432fad135f3",
+                "sha256:ade6d25bb29c4555d718ac6d1443a7386595528c33d6b133b258f65f963bb0f6",
+                "sha256:afea96f64efa98df4da6958bae37f1cbea7932c35878b185e5982821bc883369",
+                "sha256:b1579413ae492b05de5a6174574f8c44c2b9b122a42015c5292afa4be2507f28",
+                "sha256:b3451108ab861040754fa5208bca4a5496c65875710f76789a9ad27c801a0075",
+                "sha256:b9af3757495c1ee3b5c4e945c1df7be95562277c6e5bccc20a39aec50f826cd0",
+                "sha256:bc16ac425cc927d0a57d242589f87ee093884ea4804c05a13834d07c20db203c",
+                "sha256:c2910967e6ab16bf6fbeb1f771c89a7050947221ae12a5b0b60f3bca2ee19bca",
+                "sha256:c2b92812bd214018e50b6380ea3ac0c8bb01ac07fcc14c5f86a5bb25e74026e9",
+                "sha256:c2f20ce161ebdb0091a10c9ca0372e023ce24980d0e1f810f519da6f79c60800",
+                "sha256:c56d748ea50215abef7030c72b60dd723ed5b5c7e65e7bc2504e77843631c1a6",
+                "sha256:c7c133e93b405eb0d36fa430c94185bdd13c36204a8635470cccc200723c13bb",
+                "sha256:c9c6c9b2c2f80747a98f34ef491c4d7b1a8d4853937bb1492774992a120f475d",
+                "sha256:cbc8df5c6a88ba5ae385d8930da02201165408dde8d8322072e3e5ddd4f68e22",
+                "sha256:cff084c6933680d1f8b2f3b4ff5bbb88538a4aac00d199ac13f49d0698727ecb",
+                "sha256:d2045d6d9439a0078f2a34b57c7b18c4a6aef0bee37f22e4ec9f32456c852c71",
+                "sha256:d20a0ddb3e989e8807d83225a27e5c2eb2260eaa851532086e9e0fa0d5287d83",
+                "sha256:d457aed310f2670f59cc5b57dcfced452aeeed77f9da2b9763616bd57e4dbaae",
+                "sha256:d89528b4943d27029a2818f847c10c2cecc79fa9590f3cb1860459a5be7933eb",
+                "sha256:db0b2af416ba735c6304c47f75d348f498b92952f5e3e8bff449336d2728795d",
+                "sha256:deee9ca4727f53464daf089536e68b13e6104e84a37820a88b0a057b97bba2d2",
+                "sha256:df27ffddff4190667d40de7beba4a950b5ce78fe28a7dcc41d6f8a700a80a3c0",
+                "sha256:e0c95ddd4f6e9fca4e9e3afaa4f9df8552f0ba5d1004e89ef0a68e1f1f9807c7",
+                "sha256:e1c1be77bc5fb77d923850f82e55a928f8638f64a61f00ff18a67c7404faf008",
+                "sha256:e1ffa1c924e8c72778b9ccd386a7067cddf626884fd8277f503c48bb5f51c762",
+                "sha256:e2400a94f7dd9cb20cd012951a0cbf8249e3d554c63a9c0cdfd5cbb6c01d2dec",
+                "sha256:e61f091c3ba0c3578411ef505992d356a812fb200643eab27f4f70eed34a29ef",
+                "sha256:e8a701123029cc240cea61dd2d16ad57cab4691804143ce80ecd9286b464d180",
+                "sha256:eadbefd5e92ef8a345f0525b5cfd01cf4e4cc651a2cffb8f23c0dd184975d787",
+                "sha256:f32260e556a983bc5c7ed588d04c942c9a8f9c2e99213fec11a031e316874c7e",
+                "sha256:f8115e303280ba09f3898194791a153862cbf9eef722ad8f7f741987ee2a97c7",
+                "sha256:fedbdc753827cf014c01dbbee9c3be17e5a208dcd1bf8641ce2cd29580d1f0d4"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==25.1.1"
+        },
+        "questplus": {
+            "hashes": [
+                "sha256:89337d613834fa365dc4a1b25496ccf17debb0d6d5a56c6b12a647da8195ca8e",
+                "sha256:cf0902d9dd3a804ced24a515eaace82edce5ee1db8b7738bc509a3b761ae4ba6"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2023.1"
+        },
+        "referencing": {
+            "hashes": [
+                "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf",
+                "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.30.2"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa",
+                "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"
+            ],
+            "index": "pypi",
+            "version": "==2.28.2"
+        },
+        "requests-oauthlib": {
+            "hashes": [
+                "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5",
+                "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.3.1"
+        },
+        "requests-toolbelt": {
+            "hashes": [
+                "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6",
+                "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.0.0"
+        },
+        "retina-face": {
+            "hashes": [
+                "sha256:0e51c8b0f5e8a9230d16c17f30e0813d67286c8c24fbe13640cc59ef692673c5",
+                "sha256:6710df84c645641f6f085ca4344f4844bcf8608df6e383dceb341c135b2f2553"
+            ],
+            "markers": "python_full_version >= '3.5.5'",
+            "version": "==0.0.13"
+        },
+        "rich": {
+            "hashes": [
+                "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245",
+                "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==13.6.0"
+        },
+        "rpds-py": {
+            "hashes": [
+                "sha256:0525847f83f506aa1e28eb2057b696fe38217e12931c8b1b02198cfe6975e142",
+                "sha256:05942656cb2cb4989cd50ced52df16be94d344eae5097e8583966a1d27da73a5",
+                "sha256:0831d3ecdea22e4559cc1793f22e77067c9d8c451d55ae6a75bf1d116a8e7f42",
+                "sha256:0853da3d5e9bc6a07b2486054a410b7b03f34046c123c6561b535bb48cc509e1",
+                "sha256:08e6e7ff286254016b945e1ab632ee843e43d45e40683b66dd12b73791366dd1",
+                "sha256:0a38612d07a36138507d69646c470aedbfe2b75b43a4643f7bd8e51e52779624",
+                "sha256:0bedd91ae1dd142a4dc15970ed2c729ff6c73f33a40fa84ed0cdbf55de87c777",
+                "sha256:0c5441b7626c29dbd54a3f6f3713ec8e956b009f419ffdaaa3c80eaf98ddb523",
+                "sha256:0e9e976e0dbed4f51c56db10831c9623d0fd67aac02853fe5476262e5a22acb7",
+                "sha256:0fadfdda275c838cba5102c7f90a20f2abd7727bf8f4a2b654a5b617529c5c18",
+                "sha256:1096ca0bf2d3426cbe79d4ccc91dc5aaa73629b08ea2d8467375fad8447ce11a",
+                "sha256:171d9a159f1b2f42a42a64a985e4ba46fc7268c78299272ceba970743a67ee50",
+                "sha256:188912b22b6c8225f4c4ffa020a2baa6ad8fabb3c141a12dbe6edbb34e7f1425",
+                "sha256:1b4cf9ab9a0ae0cb122685209806d3f1dcb63b9fccdf1424fb42a129dc8c2faa",
+                "sha256:1e04581c6117ad9479b6cfae313e212fe0dfa226ac727755f0d539cd54792963",
+                "sha256:1fa73ed22c40a1bec98d7c93b5659cd35abcfa5a0a95ce876b91adbda170537c",
+                "sha256:2124f9e645a94ab7c853bc0a3644e0ca8ffbe5bb2d72db49aef8f9ec1c285733",
+                "sha256:240687b5be0f91fbde4936a329c9b7589d9259742766f74de575e1b2046575e4",
+                "sha256:25740fb56e8bd37692ed380e15ec734be44d7c71974d8993f452b4527814601e",
+                "sha256:27ccc93c7457ef890b0dd31564d2a05e1aca330623c942b7e818e9e7c2669ee4",
+                "sha256:281c8b219d4f4b3581b918b816764098d04964915b2f272d1476654143801aa2",
+                "sha256:2d34a5450a402b00d20aeb7632489ffa2556ca7b26f4a63c35f6fccae1977427",
+                "sha256:301bd744a1adaa2f6a5e06c98f1ac2b6f8dc31a5c23b838f862d65e32fca0d4b",
+                "sha256:30e5ce9f501fb1f970e4a59098028cf20676dee64fc496d55c33e04bbbee097d",
+                "sha256:33ab498f9ac30598b6406e2be1b45fd231195b83d948ebd4bd77f337cb6a2bff",
+                "sha256:35585a8cb5917161f42c2104567bb83a1d96194095fc54a543113ed5df9fa436",
+                "sha256:389c0e38358fdc4e38e9995e7291269a3aead7acfcf8942010ee7bc5baee091c",
+                "sha256:3acadbab8b59f63b87b518e09c4c64b142e7286b9ca7a208107d6f9f4c393c5c",
+                "sha256:3b7a64d43e2a1fa2dd46b678e00cabd9a49ebb123b339ce799204c44a593ae1c",
+                "sha256:3c8c0226c71bd0ce9892eaf6afa77ae8f43a3d9313124a03df0b389c01f832de",
+                "sha256:429349a510da82c85431f0f3e66212d83efe9fd2850f50f339341b6532c62fe4",
+                "sha256:466030a42724780794dea71eb32db83cc51214d66ab3fb3156edd88b9c8f0d78",
+                "sha256:47aeceb4363851d17f63069318ba5721ae695d9da55d599b4d6fb31508595278",
+                "sha256:48aa98987d54a46e13e6954880056c204700c65616af4395d1f0639eba11764b",
+                "sha256:4b2416ed743ec5debcf61e1242e012652a4348de14ecc7df3512da072b074440",
+                "sha256:4d0a675a7acbbc16179188d8c6d0afb8628604fc1241faf41007255957335a0b",
+                "sha256:4eb74d44776b0fb0782560ea84d986dffec8ddd94947f383eba2284b0f32e35e",
+                "sha256:4f8a1d990dc198a6c68ec3d9a637ba1ce489b38cbfb65440a27901afbc5df575",
+                "sha256:513ccbf7420c30e283c25c82d5a8f439d625a838d3ba69e79a110c260c46813f",
+                "sha256:5210a0018c7e09c75fa788648617ebba861ae242944111d3079034e14498223f",
+                "sha256:54cdfcda59251b9c2f87a05d038c2ae02121219a04d4a1e6fc345794295bdc07",
+                "sha256:56dd500411d03c5e9927a1eb55621e906837a83b02350a9dc401247d0353717c",
+                "sha256:57ec6baec231bb19bb5fd5fc7bae21231860a1605174b11585660236627e390e",
+                "sha256:5f1519b080d8ce0a814f17ad9fb49fb3a1d4d7ce5891f5c85fc38631ca3a8dc4",
+                "sha256:6174d6ad6b58a6bcf67afbbf1723420a53d06c4b89f4c50763d6fa0a6ac9afd2",
+                "sha256:68172622a5a57deb079a2c78511c40f91193548e8ab342c31e8cb0764d362459",
+                "sha256:6915fc9fa6b3ec3569566832e1bb03bd801c12cea030200e68663b9a87974e76",
+                "sha256:6b75b912a0baa033350367a8a07a8b2d44fd5b90c890bfbd063a8a5f945f644b",
+                "sha256:6f5dcb658d597410bb7c967c1d24eaf9377b0d621358cbe9d2ff804e5dd12e81",
+                "sha256:6f8d7fe73d1816eeb5378409adc658f9525ecbfaf9e1ede1e2d67a338b0c7348",
+                "sha256:7036316cc26b93e401cedd781a579be606dad174829e6ad9e9c5a0da6e036f80",
+                "sha256:7188ddc1a8887194f984fa4110d5a3d5b9b5cd35f6bafdff1b649049cbc0ce29",
+                "sha256:761531076df51309075133a6bc1db02d98ec7f66e22b064b1d513bc909f29743",
+                "sha256:7979d90ee2190d000129598c2b0c82f13053dba432b94e45e68253b09bb1f0f6",
+                "sha256:8015835494b21aa7abd3b43fdea0614ee35ef6b03db7ecba9beb58eadf01c24f",
+                "sha256:81c4d1a3a564775c44732b94135d06e33417e829ff25226c164664f4a1046213",
+                "sha256:81cf9d306c04df1b45971c13167dc3bad625808aa01281d55f3cf852dde0e206",
+                "sha256:88857060b690a57d2ea8569bca58758143c8faa4639fb17d745ce60ff84c867e",
+                "sha256:8c567c664fc2f44130a20edac73e0a867f8e012bf7370276f15c6adc3586c37c",
+                "sha256:91bd2b7cf0f4d252eec8b7046fa6a43cee17e8acdfc00eaa8b3dbf2f9a59d061",
+                "sha256:9620650c364c01ed5b497dcae7c3d4b948daeae6e1883ae185fef1c927b6b534",
+                "sha256:9b007c2444705a2dc4a525964fd4dd28c3320b19b3410da6517cab28716f27d3",
+                "sha256:9bf9acce44e967a5103fcd820fc7580c7b0ab8583eec4e2051aec560f7b31a63",
+                "sha256:a239303acb0315091d54c7ff36712dba24554993b9a93941cf301391d8a997ee",
+                "sha256:a2baa6be130e8a00b6cbb9f18a33611ec150b4537f8563bddadb54c1b74b8193",
+                "sha256:a54917b7e9cd3a67e429a630e237a90b096e0ba18897bfb99ee8bd1068a5fea0",
+                "sha256:a689e1ded7137552bea36305a7a16ad2b40be511740b80748d3140614993db98",
+                "sha256:a952ae3eb460c6712388ac2ec706d24b0e651b9396d90c9a9e0a69eb27737fdc",
+                "sha256:aa32205358a76bf578854bf31698a86dc8b2cb591fd1d79a833283f4a403f04b",
+                "sha256:b2287c09482949e0ca0c0eb68b2aca6cf57f8af8c6dfd29dcd3bc45f17b57978",
+                "sha256:b6b0e17d39d21698185097652c611f9cf30f7c56ccec189789920e3e7f1cee56",
+                "sha256:b710bf7e7ae61957d5c4026b486be593ed3ec3dca3e5be15e0f6d8cf5d0a4990",
+                "sha256:b8e11715178f3608874508f08e990d3771e0b8c66c73eb4e183038d600a9b274",
+                "sha256:b92aafcfab3d41580d54aca35a8057341f1cfc7c9af9e8bdfc652f83a20ced31",
+                "sha256:bec29b801b4adbf388314c0d050e851d53762ab424af22657021ce4b6eb41543",
+                "sha256:c694bee70ece3b232df4678448fdda245fd3b1bb4ba481fb6cd20e13bb784c46",
+                "sha256:c6b52b7028b547866c2413f614ee306c2d4eafdd444b1ff656bf3295bf1484aa",
+                "sha256:cb41ad20064e18a900dd427d7cf41cfaec83bcd1184001f3d91a1f76b3fcea4e",
+                "sha256:cd316dbcc74c76266ba94eb021b0cc090b97cca122f50bd7a845f587ff4bf03f",
+                "sha256:ced40cdbb6dd47a032725a038896cceae9ce267d340f59508b23537f05455431",
+                "sha256:d1c562a9bb72244fa767d1c1ab55ca1d92dd5f7c4d77878fee5483a22ffac808",
+                "sha256:d389ff1e95b6e46ebedccf7fd1fadd10559add595ac6a7c2ea730268325f832c",
+                "sha256:d56b1cd606ba4cedd64bb43479d56580e147c6ef3f5d1c5e64203a1adab784a2",
+                "sha256:d72a4315514e5a0b9837a086cb433b004eea630afb0cc129de76d77654a9606f",
+                "sha256:d9e7f29c00577aff6b318681e730a519b235af292732a149337f6aaa4d1c5e31",
+                "sha256:dbc25baa6abb205766fb8606f8263b02c3503a55957fcb4576a6bb0a59d37d10",
+                "sha256:e57919c32ee295a2fca458bb73e4b20b05c115627f96f95a10f9f5acbd61172d",
+                "sha256:e5bbe011a2cea9060fef1bb3d668a2fd8432b8888e6d92e74c9c794d3c101595",
+                "sha256:e6aea5c0eb5b0faf52c7b5c4a47c8bb64437173be97227c819ffa31801fa4e34",
+                "sha256:e888be685fa42d8b8a3d3911d5604d14db87538aa7d0b29b1a7ea80d354c732d",
+                "sha256:eebaf8c76c39604d52852366249ab807fe6f7a3ffb0dd5484b9944917244cdbe",
+                "sha256:efbe0b5e0fd078ed7b005faa0170da4f72666360f66f0bb2d7f73526ecfd99f9",
+                "sha256:efddca2d02254a52078c35cadad34762adbae3ff01c6b0c7787b59d038b63e0d",
+                "sha256:f05450fa1cd7c525c0b9d1a7916e595d3041ac0afbed2ff6926e5afb6a781b7f",
+                "sha256:f12d69d568f5647ec503b64932874dade5a20255736c89936bf690951a5e79f5",
+                "sha256:f45321224144c25a62052035ce96cbcf264667bcb0d81823b1bbc22c4addd194",
+                "sha256:f62581d7e884dd01ee1707b7c21148f61f2febb7de092ae2f108743fcbef5985",
+                "sha256:f8832a4f83d4782a8f5a7b831c47e8ffe164e43c2c148c8160ed9a6d630bc02a",
+                "sha256:fa35ad36440aaf1ac8332b4a4a433d4acd28f1613f0d480995f5cfd3580e90b7"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.12.0"
+        },
+        "rsa": {
+            "hashes": [
+                "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
+                "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
+            ],
+            "markers": "python_version >= '3.6' and python_version < '4'",
+            "version": "==4.9"
+        },
+        "scikit-learn": {
+            "hashes": [
+                "sha256:0834e4cec2a2e0d8978f39cb8fe1cad3be6c27a47927e1774bf5737ea65ec228",
+                "sha256:184a42842a4e698ffa4d849b6019de50a77a0aa24d26afa28fa49c9190bb144b",
+                "sha256:1beaa631434d1f17a20b1eef5d842e58c195875d2bc11901a1a70b5fe544745b",
+                "sha256:23a88883ca60c571a06278e4726b3b51b3709cfa4c93cacbf5568b22ba960899",
+                "sha256:23fb9e74b813cc2528b5167d82ed08950b11106ccf50297161875e45152fb311",
+                "sha256:250da993701da88bf475e7c5746abf1285ea0ae47e4d0917cd13afd6600bb162",
+                "sha256:25ba705ee1600ffc5df1dccd8fae129d7c6836e44ffcbb52d78536c9eaf8fcf9",
+                "sha256:28b2bd6a1419acd522ff45d282c8ba23dbccb5338802ab0ee12baa4ade0aba4c",
+                "sha256:2ee2c649f2231b68511aabb0dc827edd8936aad682acc6263c34aed11bc95dac",
+                "sha256:30e27721adc308e8fd9f419f43068e43490005f911edf4476a9e585059fa8a83",
+                "sha256:38814f66285318f2e241305cca545eaa9b4126c65aa5dd78c69371f235f78e2b",
+                "sha256:40f3ff68c505cb9d1f3693397c73991875d609da905087e00e7b4477645ec67b",
+                "sha256:4d3a19166d4e1cdfcab975c68f471e046ce01e74c42a9a33fa89a14c2fcedf60",
+                "sha256:4e1ea0bc1706da45589bcf2490cde6276490a1b88f9af208dbb396fdc3a0babf",
+                "sha256:5546a8894a0616e92489ef995b39a0715829f3df96e801bb55cbf196be0d9649",
+                "sha256:5699cded6c0685426433c7e5afe0fecad80ec831ec7fa264940e50c796775cc5",
+                "sha256:6785b8a3093329bf90ac01801be5525551728ae73edb11baa175df660820add4",
+                "sha256:680b65b3caee469541385d2ca5b03ff70408f6c618c583948312f0d2125df680",
+                "sha256:6b63ca2b0643d30fbf9d25d93017ed3fb8351f31175d82d104bfec60cba7bb87",
+                "sha256:6d1c1394e38a3319ace620381f6f23cc807d8780e9915c152449a86fc8f1db21",
+                "sha256:701181792a28c82fecae12adb5d15d0ecf57bffab7cf4bdbb52c7b3fd428d540",
+                "sha256:748f2bd632d6993e8918d43f1a26c380aeda4e122a88840d4c3a9af99d4239fe",
+                "sha256:83c772fa8c64776ad769fd764752c8452844307adcf10dee3adcc43988260f21",
+                "sha256:867023a044fdfe59e5014a7fec7a3086a8928f10b5dce9382eedf4135f6709a2",
+                "sha256:8e9dd76c7274055d1acf4526b8efb16a3531c26dcda714a0c16da99bf9d41900",
+                "sha256:bc7073e025b62c1067cbfb76e69d08650c6b9d7a0e7afdfa20cb92d4afe516f6",
+                "sha256:bef51978a51ec19977700fe7b86aecea49c825884f3811756b74a3b152bb4e35",
+                "sha256:cd55c6fbef7608dbce1f22baf289dfcc6eb323247daa3c3542f73d389c724786",
+                "sha256:ceb0008f345188aa236e49c973dc160b9ed504a3abd7b321a0ecabcb669be0bd",
+                "sha256:d395730f26d8fc752321f1953ddf72647c892d8bed74fad4d7c816ec9b602dfa",
+                "sha256:da29d2e379c396a63af5ed4b671ad2005cd690ac373a23bee5a0f66504e05272",
+                "sha256:da5a2e95fef9805b1750e4abda4e834bf8835d26fc709a391543b53feee7bd0e",
+                "sha256:de897720173b26842e21bed54362f5294e282422116b61cd931d4f5d870b9855",
+                "sha256:e9535e867281ae6987bb80620ba14cf1649e936bfe45f48727b978b7a2dbe835",
+                "sha256:ee47f68d973cee7009f06edb956f2f5588a0f230f24a2a70175fd0ecf36e2653",
+                "sha256:f17420a8e3f40129aeb7e0f5ee35822d6178617007bb8f69521a2cefc20d5f00",
+                "sha256:f4931f2a6c06e02c6c17a05f8ae397e2545965bc7a0a6cb38c8cd7d4fba8624d",
+                "sha256:f5644663987ee221f5d1f47a593271b966c271c236fe05634e6bdc06041b5a2b",
+                "sha256:f5d4231af7199531e77da1b78a4cc6b3d960a00b1ec672578ac818aae2b9c35d",
+                "sha256:fc0a72237f0c56780cf550df87201a702d3bdcbbb23c6ef7d54c19326fa23f19",
+                "sha256:fd3480c982b9e616b9f76ad8587804d3f4e91b4e2a6752e7dafb8a2e1f541098",
+                "sha256:fd3ee69d36d42a7dcbb17e355a5653af5fd241a7dfd9133080b3dde8d9e2aafb"
+            ],
+            "index": "pypi",
+            "version": "==1.1.3"
+        },
+        "scipy": {
+            "hashes": [
+                "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415",
+                "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f",
+                "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd",
+                "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f",
+                "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d",
+                "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601",
+                "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5",
+                "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88",
+                "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f",
+                "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e",
+                "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2",
+                "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353",
+                "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35",
+                "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6",
+                "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea",
+                "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35",
+                "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1",
+                "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9",
+                "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5",
+                "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019",
+                "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"
+            ],
+            "index": "pypi",
+            "version": "==1.10.1"
+        },
+        "seaborn": {
+            "hashes": [
+                "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139",
+                "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"
+            ],
+            "index": "pypi",
+            "version": "==0.12.2"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a",
+                "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==67.6.1"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.16.0"
+        },
+        "smmap": {
+            "hashes": [
+                "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62",
+                "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==5.0.1"
+        },
+        "sniffio": {
+            "hashes": [
+                "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
+                "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.3.0"
+        },
+        "sounddevice": {
+            "hashes": [
+                "sha256:3236b78f15f0415bdf006a620cef073d0c0522851d66f4a961ed6d8eb1482fe9",
+                "sha256:5de768ba6fe56ad2b5aaa2eea794b76b73e427961c95acad2ee2ed7f866a4b20",
+                "sha256:7830d4f8f8570f2e5552942f81d96999c5fcd9a0b682d6fc5d5c5529df23be2c",
+                "sha256:8b0b806c205dd3e3cd5a97262b2482624fd21db7d47083b887090148a08051c8",
+                "sha256:e3ba6e674ffa8f79a591d744a1d4ab922fe5bdfd4faf8b25069a08e051010b7b"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.4.6"
+        },
+        "soundfile": {
+            "hashes": [
+                "sha256:074247b771a181859d2bc1f98b5ebf6d5153d2c397b86ee9e29ba602a8dfe2a6",
+                "sha256:0d86924c00b62552b650ddd28af426e3ff2d4dc2e9047dae5b3d8452e0a49a77",
+                "sha256:2dc3685bed7187c072a46ab4ffddd38cef7de9ae5eb05c03df2ad569cf4dacbc",
+                "sha256:59dfd88c79b48f441bbf6994142a19ab1de3b9bb7c12863402c2bc621e49091a",
+                "sha256:828a79c2e75abab5359f780c81dccd4953c45a2c4cd4f05ba3e233ddf984b882",
+                "sha256:bceaab5c4febb11ea0554566784bcf4bc2e3977b53946dda2b12804b4fe524a8",
+                "sha256:d922be1563ce17a69582a352a86f28ed8c9f6a8bc951df63476ffc310c064bfa",
+                "sha256:e8e1017b2cf1dda767aef19d2fd9ee5ebe07e050d430f77a0a7c66ba08b8cdae"
+            ],
+            "version": "==0.12.1"
+        },
+        "soupsieve": {
+            "hashes": [
+                "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8",
+                "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.4.1"
+        },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:03be6f3cb66e69fb3a09b5ea89d77e4bc942f3bf84b207dba84666a26799c166",
+                "sha256:048509d7f3ac27b83ad82fd96a1ab90a34c8e906e4e09c8d677fc531d12c23c5",
+                "sha256:07764b240645627bc3e82596435bd1a1884646bfc0721642d24c26b12f1df194",
+                "sha256:0fdbb8e9d4e9003f332a93d6a37bca48ba8095086c97a89826a136d8eddfc455",
+                "sha256:10edbb92a9ef611f01b086e271a9f6c1c3e5157c3b0c5ff62310fb2187acbd4a",
+                "sha256:14a3879853208a242b5913f3a17c6ac0eae9dc210ff99c8f10b19d4a1ed8ed9b",
+                "sha256:16ee6fea316790980779268da47a9260d5dd665c96f225d28e7750b0bb2e2a04",
+                "sha256:1e2a42017984099ef6f56438a6b898ce0538f6fadddaa902870c5aa3e1d82583",
+                "sha256:28297aa29e035f29cba6b16aacd3680fbc6a9db682258d5f2e7b49ec215dbe40",
+                "sha256:28fda5a69d6182589892422c5a9b02a8fd1125787aab1d83f1392aa955bf8d0a",
+                "sha256:299b5c5c060b9fbe51808d0d40d8475f7b3873317640b9b7617c7f988cf59fda",
+                "sha256:2bba39b12b879c7b35cde18b6e14119c5f1a16bd064a48dd2ac62d21366a5e17",
+                "sha256:32ab09f2863e3de51529aa84ff0e4fe89a2cb1bfbc11e225b6dbc60814e44c94",
+                "sha256:45e799c1a41822eba6bee4e59b0e38764e1a1ee69873ab2889079865e9ea0e23",
+                "sha256:511d4abc823152dec49461209607bbfb2df60033c8c88a3f7c93293b8ecbb13d",
+                "sha256:557675e0befafa08d36d7a9284e8761c97490a248474d778373fb96b0d7fd8de",
+                "sha256:6572d7c96c2e3e126d0bb27bfb1d7e2a195b68d951fcc64c146b94f088e5421a",
+                "sha256:684e5c773222781775c7f77231f412633d8af22493bf35b7fa1029fdf8066d10",
+                "sha256:6a94632ba26a666e7be0a7d7cc3f7acab622a04259a3aa0ee50ff6d44ba9df0d",
+                "sha256:6b6d807c76c20b4bc143a49ad47782228a2ac98bdcdcb069da54280e138847fc",
+                "sha256:7120a2f72599d4fed7c001fa1cbbc5b4d14929436135768050e284f53e9fbe5e",
+                "sha256:71d4bf7768169c4502f6c2b0709a02a33703544f611810fb0c75406a9c576ee1",
+                "sha256:795b5b9db573d3ed61fae74285d57d396829e3157642794d3a8f72ec2a5c719b",
+                "sha256:7a4df53472c9030a8ddb1cce517757ba38a7a25699bbcabd57dcc8a5d53f324e",
+                "sha256:8f216a51451a0a0466e082e163591f6dcb2f9ec182adb3f1f4b1fd3688c7582c",
+                "sha256:95fc02f7fc1f3199aaa47a8a757437134cf618e9d994c84effd53f530c38586f",
+                "sha256:989c62b96596b7938cbc032e39431e6c2d81b635034571d6a43a13920852fb65",
+                "sha256:998e782c8d9fd57fa8704d149ccd52acf03db30d7dd76f467fd21c1c21b414fa",
+                "sha256:9a198f690ac12a3a807e03a5a45df6a30cd215935f237a46f4248faed62e69c8",
+                "sha256:a6c3929df5eeaf3867724003d5c19fed3f0c290f3edc7911616616684f200ecf",
+                "sha256:bb2797fee8a7914fb2c3dc7de404d3f96eb77f20fc60e9ee38dc6b0ca720f2c2",
+                "sha256:bd988b3362d7e586ef581eb14771bbb48793a4edb6fcf62da75d3f0f3447060b",
+                "sha256:ca8ab6748e3ec66afccd8b23ec2f92787a58d5353ce9624dccd770427ee67c82",
+                "sha256:dbe57f39f531c5d68d5594ea4613daa60aba33bb51a8cc42f96f17bbd6305e8d",
+                "sha256:dcfb480bfc9e1fab726003ae00a6bfc67a29bad275b63a4e36d17fe7f13a624e",
+                "sha256:dd45c60cc4f6d68c30d5179e2c2c8098f7112983532897566bb69c47d87127d3",
+                "sha256:dde4d02213f1deb49eaaf8be8a6425948963a7af84983b3f22772c63826944de",
+                "sha256:e3b67bda733da1dcdccaf354e71ef01b46db483a4f6236450d3f9a61efdba35a",
+                "sha256:e98ef1babe34f37f443b7211cd3ee004d9577a19766e2dbacf62fce73c76245a",
+                "sha256:f80915681ea9001f19b65aee715115f2ad310730c8043127cf3e19b3009892dd",
+                "sha256:fc700b862e0a859a37faf85367e205e7acaecae5a098794aff52fdd8aea77b12"
+            ],
+            "index": "pypi",
+            "version": "==1.4.47"
+        },
+        "starlette": {
+            "hashes": [
+                "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd",
+                "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.26.1"
+        },
+        "streamlit": {
+            "hashes": [
+                "sha256:cca04f6d95b14b7bc37f0cabaf27504b86af6b4e1af98d73acc80ecdcfbcc492",
+                "sha256:f41c4e590299279a910c6a874aabc1428eda074c5f9d944403d2e192fce2ebb0"
+            ],
+            "index": "pypi",
+            "version": "==1.28.1"
+        },
+        "tables": {
+            "hashes": [
+                "sha256:0295123272bb49efbebdc9b1e2b72baa99c5761b78fccacedbf44c52a5fa51ac",
+                "sha256:1813c0eced77540598987db32ce9e619d02b6032acdc3f59590d83c13bdb910c",
+                "sha256:1f725f69d49f414736de24616b4ffa400127b86417bd14a11854aacd2a505b4d",
+                "sha256:22084019437c504917ba8c0b2af75419e3d5c8ffc6d2ef4cd44031f06939518c",
+                "sha256:282a0747b3ce4e3108bcd443361e031c9817bf7e84358317723a51b9c02c5655",
+                "sha256:48331503cd509c9f1f95cf2f5c64a57c48c0aa5141423f0eca352965c4f9bf81",
+                "sha256:4d1f2c947d63019db20728c6ecec39a1c900be00a65cae8025ac770148b641e8",
+                "sha256:50140091af9d60eb3f806d3ee43f542beae569888c37ae96d6a1c887c389d8c8",
+                "sha256:784c1ffe7f972e69a9c97c0f164064e43617727668df4333802a7f23cfb06ee3",
+                "sha256:a64ce39652a2e2934f6d41500b2c6f8d4922e2022f1361e2302f3e85df4e2393",
+                "sha256:aa176e1c72b0f935b0e607218ea8302378a39ed4fef5a544ebbd8d0523b56b86",
+                "sha256:af92f1e63b9fcadea621ab544540b7312553ea4f9456cf3d2728b48346fa557c",
+                "sha256:b49015aa8f576c6d5108c4aeb4d430bfcfc91ee8d0cca4d03e574e5485ffdc8b",
+                "sha256:cb89fab4a3c3cd98bd781913234e1f67464ff6e17662180cf718e67645a09271",
+                "sha256:e346249116b2eb95dd9277336c12f0d10d5328a5a3e8e16c74faa3c815817dc3",
+                "sha256:f482aaaa4b12d394421013cd4617d3e8a53a8d4a7a872454f7a13fb16c51a68e",
+                "sha256:f49e899247b541ed69d12fef10b5505b97243317a91b93927328c19a15d38671"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==3.9.1"
+        },
+        "tenacity": {
+            "hashes": [
+                "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a",
+                "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.2.3"
+        },
+        "tensorboard": {
+            "hashes": [
+                "sha256:811ab0d27a139445836db9fd4f974424602c3dce12379364d379bcba7c783a68"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.12.2"
+        },
+        "tensorboard-data-server": {
+            "hashes": [
+                "sha256:64aa1be7c23e80b1a42c13b686eb0875bb70f5e755f4d2b8de5c1d880cf2267f",
+                "sha256:753d4214799b31da7b6d93837959abebbc6afa86e69eacf1e9a317a48daa31eb",
+                "sha256:eb7fa518737944dbf4f0cf83c2e40a7ac346bf91be2e6a0215de98be74e85454"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.7.0"
+        },
+        "tensorboard-plugin-wit": {
+            "hashes": [
+                "sha256:ff26bdd583d155aa951ee3b152b3d0cffae8005dc697f72b44a8e8c2a77a8cbe"
+            ],
+            "version": "==1.8.1"
+        },
+        "tensorflow": {
+            "hashes": [
+                "sha256:020d6a54cb26020bdc71a7bae8ee35be05096f63e773dc517f6e87c49de62c50",
+                "sha256:23850332f1f9f778d697c9dba63ca52be72cb73363e75ad358f07ddafef63c01",
+                "sha256:31f81eb8adaeb558963f5d8b47dbfcc398d898f0857bf3de6b6484350236b7b5",
+                "sha256:357d9d2851188a8d27ee195345b4d175cad970150d1344ba9d9fcc4bf2b68336",
+                "sha256:42fc2635e9420faee781a16bd393126f29cd39aa2b9d02901f24d8497bd6f958",
+                "sha256:4afc2dd57435f29ebe249eb5f595d89b0e73be94922eeb7110aa6280a332837c",
+                "sha256:6e7641e2a6e32f31ff233495478a9cc86b7c038140eab714a61eeddbbbb327c3",
+                "sha256:6ec4a2934ea19e92f27a9668ece43025ed5efe14b5d19be53b07692bc8a4189d",
+                "sha256:76414355e420edb9154b4e72113eef5813ccb71701fda959afbbc1eebe3099bd",
+                "sha256:91dccda42c03569d8c787190482a11ecae3b9b173aaa9166f0ab20cecc9c31f4",
+                "sha256:9f70a8f9ab46e5ed436850aa60d1cd40645f5c669e14bcad48915dc1f597dda2",
+                "sha256:a7194e744c5a7f3e759ecb949527b4a07718a6d1110e6e82fd4ce0c5586a7d4a",
+                "sha256:be4ac0dfcc7a16f6df2bc19bd322e312235ab3f7b0c7297f96c92c44bb14d2a1",
+                "sha256:c5193ddb3bb5120cb445279beb08ed9e74a85a4eeb2485550d6fb707a89d9a88",
+                "sha256:c8001210df7202ef6267150865b0b79f834c3ca69ee3132277de8eeb994dffde",
+                "sha256:e29fcf6cfd069aefb4b44f357cccbb4415a5a3d7b5b516eaf4450062fe40021e"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.12.0"
+        },
+        "tensorflow-estimator": {
+            "hashes": [
+                "sha256:59b191bead4883822de3d63ac02ace11a83bfe6c10d64d0c4dfde75a50e60ca1"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.12.0"
+        },
+        "tensorflow-intel": {
+            "hashes": [
+                "sha256:2789b97028ffbfa64846a1736da0021b7be22567d2cdfff826583e63c6d21485",
+                "sha256:2c3ece439d589362374d6e9f7775d2fac4591e0d9fc22a431f2eeaa4a0d2994a",
+                "sha256:816a6b9018d1ae0defe94508aab2512228fb6a54ab51f5f4f025c857cfe5c7e5",
+                "sha256:a1eaab8edc87207c9510799aada88c55019055095ae0de0fea0f21a13fae4424"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==2.12.0"
+        },
+        "tensorflow-io-gcs-filesystem": {
+            "hashes": [
+                "sha256:20e3ee5df01f2bd81d37fc715816c329b7533ccca967c47946eb458a5b7a7280",
+                "sha256:359134ecbd3bf938bb0cf65be4526106c30da461b2e2ce05446a229ed35f6832",
+                "sha256:37c40e3c4ee1f8dda3b545deea6b8839192c82037d8021db9f589908034ad975",
+                "sha256:4bb37d23f21c434687b11059cb7ffd094d52a7813368915ba1b7057e3c16e414",
+                "sha256:68b89ef9f63f297de1cd9d545bc45dddc7d8fe12bcda4266279b244e8cf3b7c0",
+                "sha256:8909c4344b0e96aa356230ab460ffafe5900c33c1aaced65fafae71d177a1966",
+                "sha256:961353b38c76471fa296bb7d883322c66b91415e7d47087236a6706db3ab2758",
+                "sha256:97ebb9a8001a38f615aa1f90d2e998b7bd6eddae7aafc92897833610b039401b",
+                "sha256:a71421f8d75a093b6aac65b4c8c8d2f768c3ca6215307cf8c16192e62d992bcf",
+                "sha256:a7e8d4bd0a25de7637e562997c011294d7ea595a76f315427a5dd522d56e9d49",
+                "sha256:b4ebb30ad7ce5f3769e3d959ea99bd95d80a44099bcf94da6042f9755ac6e850",
+                "sha256:b658b33567552f155af2ed848130f787bfda29381fa78cd905d5ee8254364f3c",
+                "sha256:bd628609b77aee0e385eadf1628222486f19b8f1d81b5f0a344f2470204df116",
+                "sha256:cb7459c15608fe42973a78e4d3ad7ac79cfc7adae1ccb1b1846db3165fbc081a",
+                "sha256:e3933059b1c53e062075de2e355ec136b655da5883c3c26736c45dfeb1901945",
+                "sha256:e417faf8755aafe52d8f8c6b5ae5bae6e4fae8326ee3acd5e9181b83bbfbae87",
+                "sha256:e6d8cc7b14ade870168b9704ee44f9c55b468b9a00ed40e12d20fffd321193b5",
+                "sha256:f0adfbcd264262797d429311843733da2d5c1ffb119fbfa6339269b6c0414113",
+                "sha256:fbcfb4aa2eaa9a3038d2487e570ff93feb1dbe51c3a4663d7d9ab9f9a9f9a9d8"
+            ],
+            "markers": "python_version < '3.12' and python_version >= '3.7'",
+            "version": "==0.31.0"
+        },
+        "termcolor": {
+            "hashes": [
+                "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7",
+                "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.2.0"
+        },
+        "threadpoolctl": {
+            "hashes": [
+                "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b",
+                "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.1.0"
+        },
+        "toml": {
+            "hashes": [
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.10.2"
+        },
+        "toolz": {
+            "hashes": [
+                "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f",
+                "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==0.12.0"
+        },
+        "tornado": {
+            "hashes": [
+                "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f",
+                "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5",
+                "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d",
+                "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3",
+                "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2",
+                "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a",
+                "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16",
+                "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a",
+                "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17",
+                "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0",
+                "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==6.3.3"
+        },
+        "tqdm": {
+            "hashes": [
+                "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5",
+                "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.65.0"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
+                "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.5.0"
+        },
+        "tzdata": {
+            "hashes": [
+                "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a",
+                "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"
+            ],
+            "markers": "python_version >= '2'",
+            "version": "==2023.3"
+        },
+        "tzlocal": {
+            "hashes": [
+                "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8",
+                "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==5.2"
+        },
+        "ujson": {
+            "hashes": [
+                "sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c",
+                "sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7",
+                "sha256:0fe1b7edaf560ca6ab023f81cbeaf9946a240876a993b8c5a21a1c539171d903",
+                "sha256:102bf31c56f59538cccdfec45649780ae00657e86247c07edac434cb14d5388c",
+                "sha256:11da6bed916f9bfacf13f4fc6a9594abd62b2bb115acfb17a77b0f03bee4cfd5",
+                "sha256:16fde596d5e45bdf0d7de615346a102510ac8c405098e5595625015b0d4b5296",
+                "sha256:193349a998cd821483a25f5df30b44e8f495423840ee11b3b28df092ddfd0f7f",
+                "sha256:20768961a6a706170497129960762ded9c89fb1c10db2989c56956b162e2a8a3",
+                "sha256:27a2a3c7620ebe43641e926a1062bc04e92dbe90d3501687957d71b4bdddaec4",
+                "sha256:2873d196725a8193f56dde527b322c4bc79ed97cd60f1d087826ac3290cf9207",
+                "sha256:299a312c3e85edee1178cb6453645217ba23b4e3186412677fa48e9a7f986de6",
+                "sha256:2a64cc32bb4a436e5813b83f5aab0889927e5ea1788bf99b930fad853c5625cb",
+                "sha256:2b852bdf920fe9f84e2a2c210cc45f1b64f763b4f7d01468b33f7791698e455e",
+                "sha256:2e72ba76313d48a1a3a42e7dc9d1db32ea93fac782ad8dde6f8b13e35c229130",
+                "sha256:3659deec9ab9eb19e8646932bfe6fe22730757c4addbe9d7d5544e879dc1b721",
+                "sha256:3b27a8da7a080add559a3b73ec9ebd52e82cc4419f7c6fb7266e62439a055ed0",
+                "sha256:3f9b63530a5392eb687baff3989d0fb5f45194ae5b1ca8276282fb647f8dcdb3",
+                "sha256:407d60eb942c318482bbfb1e66be093308bb11617d41c613e33b4ce5be789adc",
+                "sha256:40931d7c08c4ce99adc4b409ddb1bbb01635a950e81239c2382cfe24251b127a",
+                "sha256:48c7d373ff22366eecfa36a52b9b55b0ee5bd44c2b50e16084aa88b9de038916",
+                "sha256:4ddeabbc78b2aed531f167d1e70387b151900bc856d61e9325fcdfefb2a51ad8",
+                "sha256:5ac97b1e182d81cf395ded620528c59f4177eee024b4b39a50cdd7b720fdeec6",
+                "sha256:5ce24909a9c25062e60653073dd6d5e6ec9d6ad7ed6e0069450d5b673c854405",
+                "sha256:69b3104a2603bab510497ceabc186ba40fef38ec731c0ccaa662e01ff94a985c",
+                "sha256:6a4dafa9010c366589f55afb0fd67084acd8added1a51251008f9ff2c3e44042",
+                "sha256:6d230d870d1ce03df915e694dcfa3f4e8714369cce2346686dbe0bc8e3f135e7",
+                "sha256:78e318def4ade898a461b3d92a79f9441e7e0e4d2ad5419abed4336d702c7425",
+                "sha256:7a42baa647a50fa8bed53d4e242be61023bd37b93577f27f90ffe521ac9dc7a3",
+                "sha256:7cba16b26efe774c096a5e822e4f27097b7c81ed6fb5264a2b3f5fd8784bab30",
+                "sha256:7d8283ac5d03e65f488530c43d6610134309085b71db4f675e9cf5dff96a8282",
+                "sha256:7ecc33b107ae88405aebdb8d82c13d6944be2331ebb04399134c03171509371a",
+                "sha256:9249fdefeb021e00b46025e77feed89cd91ffe9b3a49415239103fc1d5d9c29a",
+                "sha256:9399eaa5d1931a0ead49dce3ffacbea63f3177978588b956036bfe53cdf6af75",
+                "sha256:94c7bd9880fa33fcf7f6d7f4cc032e2371adee3c5dba2922b918987141d1bf07",
+                "sha256:9571de0c53db5cbc265945e08f093f093af2c5a11e14772c72d8e37fceeedd08",
+                "sha256:9721cd112b5e4687cb4ade12a7b8af8b048d4991227ae8066d9c4b3a6642a582",
+                "sha256:9ab282d67ef3097105552bf151438b551cc4bedb3f24d80fada830f2e132aeb9",
+                "sha256:9d9707e5aacf63fb919f6237d6490c4e0244c7f8d3dc2a0f84d7dec5db7cb54c",
+                "sha256:a70f776bda2e5072a086c02792c7863ba5833d565189e09fabbd04c8b4c3abba",
+                "sha256:a89cf3cd8bf33a37600431b7024a7ccf499db25f9f0b332947fbc79043aad879",
+                "sha256:a8c91b6f4bf23f274af9002b128d133b735141e867109487d17e344d38b87d94",
+                "sha256:ad24ec130855d4430a682c7a60ca0bc158f8253ec81feed4073801f6b6cb681b",
+                "sha256:ae7f4725c344bf437e9b881019c558416fe84ad9c6b67426416c131ad577df67",
+                "sha256:b748797131ac7b29826d1524db1cc366d2722ab7afacc2ce1287cdafccddbf1f",
+                "sha256:bdf04c6af3852161be9613e458a1fb67327910391de8ffedb8332e60800147a2",
+                "sha256:bf5737dbcfe0fa0ac8fa599eceafae86b376492c8f1e4b84e3adf765f03fb564",
+                "sha256:c4e7bb7eba0e1963f8b768f9c458ecb193e5bf6977090182e2b4f4408f35ac76",
+                "sha256:d524a8c15cfc863705991d70bbec998456a42c405c291d0f84a74ad7f35c5109",
+                "sha256:d53039d39de65360e924b511c7ca1a67b0975c34c015dd468fca492b11caa8f7",
+                "sha256:d6f84a7a175c75beecde53a624881ff618e9433045a69fcfb5e154b73cdaa377",
+                "sha256:e0147d41e9fb5cd174207c4a2895c5e24813204499fd0839951d4c8784a23bf5",
+                "sha256:e3673053b036fd161ae7a5a33358ccae6793ee89fd499000204676baafd7b3aa",
+                "sha256:e54578fa8838ddc722539a752adfce9372474114f8c127bb316db5392d942f8b",
+                "sha256:eb0142f6f10f57598655340a3b2c70ed4646cbe674191da195eb0985a9813b83",
+                "sha256:efeddf950fb15a832376c0c01d8d7713479fbeceaed1eaecb2665aa62c305aec",
+                "sha256:f26629ac531d712f93192c233a74888bc8b8212558bd7d04c349125f10199fcf",
+                "sha256:f2e385a7679b9088d7bc43a64811a7713cc7c33d032d020f757c54e7d41931ae",
+                "sha256:f3554eaadffe416c6f543af442066afa6549edbc34fe6a7719818c3e72ebfe95",
+                "sha256:f4511560d75b15ecb367eef561554959b9d49b6ec3b8d5634212f9fed74a6df1",
+                "sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f",
+                "sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==5.8.0"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305",
+                "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+            "version": "==1.26.15"
+        },
+        "uvicorn": {
+            "hashes": [
+                "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032",
+                "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"
+            ],
+            "index": "pypi",
+            "version": "==0.21.1"
+        },
+        "validators": {
+            "hashes": [
+                "sha256:61cf7d4a62bbae559f2e54aed3b000cea9ff3e2fdbe463f51179b92c58c9585a",
+                "sha256:77b2689b172eeeb600d9605ab86194641670cdb73b60afd577142a9397873370"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.22.0"
+        },
+        "watchdog": {
+            "hashes": [
+                "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a",
+                "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100",
+                "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8",
+                "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc",
+                "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae",
+                "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41",
+                "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0",
+                "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f",
+                "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c",
+                "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9",
+                "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3",
+                "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709",
+                "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83",
+                "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759",
+                "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9",
+                "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3",
+                "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7",
+                "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f",
+                "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346",
+                "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674",
+                "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397",
+                "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96",
+                "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d",
+                "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a",
+                "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64",
+                "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44",
+                "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"
+            ],
+            "markers": "platform_system != 'Darwin'",
+            "version": "==3.0.0"
+        },
+        "websockets": {
+            "hashes": [
+                "sha256:007ed0d62f7e06eeb6e3a848b0d83b9fbd9e14674a59a61326845f27d20d7452",
+                "sha256:07cc20655fb16aeef1a8f03236ba8671c61d332580b996b6396a5b7967ba4b3d",
+                "sha256:0929c2ebdf00cedda77bf77685693e38c269011236e7c62182fee5848c29a4fa",
+                "sha256:12180bc1d72c6a9247472c1dee9dfd7fc2e23786f25feee7204406972d8dab39",
+                "sha256:1cb23597819f68ac6a6d133a002a1b3ef12a22850236b083242c93f81f206d5a",
+                "sha256:25ea5dbd3b00c56b034639dc6fe4d1dd095b8205bab1782d9a47cb020695fdf4",
+                "sha256:2796f097841619acf053245f266a4f66cb27c040f0d9097e5f21301aab95ff43",
+                "sha256:29282631da3bfeb5db497e4d3d94d56ee36222fbebd0b51014e68a2e70736fb1",
+                "sha256:2a58e83f82098d062ae5d4cbe7073b8783999c284d6f079f2fefe87cd8957ac8",
+                "sha256:349dd1fa56a30d530555988be98013688de67809f384671883f8bf8b8c9de984",
+                "sha256:369410925b240b30ef1c1deadbd6331e9cd865ad0b8966bf31e276cc8e0da159",
+                "sha256:385c5391becb9b58e0a4f33345e12762fd857ccf9fbf6fee428669929ba45e4c",
+                "sha256:3a88375b648a2c479532943cc19a018df1e5fcea85d5f31963c0b22794d1bdc1",
+                "sha256:3cf18bbd44b36749b7b66f047a30a40b799b8c0bd9a1b9173cba86a234b4306b",
+                "sha256:3d30cc1a90bcbf9e22e1f667c1c5a7428e2d37362288b4ebfd5118eb0b11afa9",
+                "sha256:42aa05e890fcf1faed8e535c088a1f0f27675827cbacf62d3024eb1e6d4c9e0c",
+                "sha256:43e0de552be624e5c0323ff4fcc9f0b4a9a6dc6e0116b8aa2cbb6e0d3d2baf09",
+                "sha256:45a85dc6b3ff76239379feb4355aadebc18d6e587c8deb866d11060755f4d3ea",
+                "sha256:4fe2aed5963ca267c40a2d29b1ee4e8ab008ac8d5daa284fdda9275201b8a334",
+                "sha256:52ba83ea132390e426f9a7b48848248a2dc0e7120ca8c65d5a8fc1efaa4eb51b",
+                "sha256:53b8e1ee01eb5b8be5c8a69ae26b0820dbc198d092ad50b3451adc3cdd55d455",
+                "sha256:54d084756c50dfc8086dce97b945f210ca43950154e1e04a44a30c6e6a2bcbb1",
+                "sha256:5d4f4b341100d313b08149d7031eb6d12738ac758b0c90d2f9be8675f401b019",
+                "sha256:5d68bd2a3e9fff6f7043c0a711cb1ebba9f202c196a3943d0c885650cd0b6464",
+                "sha256:5d8d5d17371ed9eb9f0e3a8d326bdf8172700164c2e705bc7f1905a719a189be",
+                "sha256:5ffe6fc5e5fe9f2634cdc59b805e4ba1fcccf3a5622f5f36c3c7c287f606e283",
+                "sha256:60a19d4ff5f451254f8623f6aa4169065f73a50ec7b59ab6b9dcddff4aa00267",
+                "sha256:718d19c494637f28e651031b3df6a791b9e86e0097c65ed5e8ec49b400b1210e",
+                "sha256:79b6548e57ab18f071b9bfe3ffe02af7184dd899bc674e2817d8fe7e9e7489ec",
+                "sha256:84e92dbac318a84fef722f38ca57acef19cbb89527aba5d420b96aa2656970ee",
+                "sha256:85b4127f7da332feb932eee833c70e5e1670469e8c9de7ef3874aa2a91a6fbb2",
+                "sha256:87ae582cf2319e45bc457a57232daded27a3c771263cab42fb8864214bbd74ea",
+                "sha256:892959b627eedcdf98ac7022f9f71f050a59624b380b67862da10c32ea3c221a",
+                "sha256:9d91279d57f6546eaf43671d1de50621e0578f13c2f17c96c458a72d170698d7",
+                "sha256:a01c674e0efe0f14aec7e722ed0e0e272fa2f10e8ea8260837e1f4f5dc4b3e53",
+                "sha256:a4667d4e41fa37fa3d836b2603b8b40d6887fa4838496d48791036394f7ace39",
+                "sha256:a797da96d4127e517a5cb0965cd03fd6ec21e02667c1258fa0579501537fbe5c",
+                "sha256:a88815a0c6253ad1312ef186620832fb347706c177730efec34e3efe75e0e248",
+                "sha256:a8d9793f3fb0da16232503df14411dabafed5a81fc9077dc430cfc6f60e71179",
+                "sha256:ac042e8ba9d7f2618e84af27927fdce0f3e03528eb74f343977486c093868389",
+                "sha256:ae59a9f0a77ecb0cbdedea7d206a547ff136e8bfbc7d2d98772fb02d398797bb",
+                "sha256:aedd94422745da60672a901f53de1f50b16e85408b18672b9b210db4a776b5a6",
+                "sha256:aef1602db81096ce3d3847865128c8879635bdad7963fb2b7df290edb9e9150a",
+                "sha256:b0ed24a3aa4213029e100257e5e73c5f912e70ca35630081de94b7f9e2cf4a9b",
+                "sha256:b138f4bf8a64c344e12c76283dac279d11adab89ac62ae4a32ac8490d3c94832",
+                "sha256:b91657b65355954e47f0df874917fa200426b3a7f4e68073326a8cfc2f6deef8",
+                "sha256:c7fdfbed727ce6b4b5e6622d15a6efb2098b2d9e22ba4dc54b2e3ce80f982045",
+                "sha256:c90343fd0774749d23c1891dd8b3e9210f9afd30986673ce0f9d5857f5cb1562",
+                "sha256:ceeef57b9aec8f27e523de4da73c518ece7721aefe7064f18aa28baabfe61b94",
+                "sha256:cfd0b9b18d64c51e5cd322e16b5bf4fe490db65c9f7b18fd5382c824062ead7e",
+                "sha256:d4e0990b6a04b07095c969969da659eecf9069cf8e7b8f49c8f5ee1bb50e3352",
+                "sha256:d5a3022f9291bf2d35ebf65929297d625e68effd3a5647b8eb8b89d51b09394c",
+                "sha256:d5a6fa353b5ef36970c3bd1cd7cecbc08bb8f2f1a3d008b0691208cf34ebf5b0",
+                "sha256:d5f3d0d177b3db3d1d02cce7ba6c0063586499ac28afe0c992be74ffc40d9257",
+                "sha256:db234da3aff01e8483cf0015b75486c04d50dbf90004bd3e5b46d384e1bd6c9e",
+                "sha256:db78535b791840a584c48cf3f4215eae38a7e2f43271ecd27ce4ba8a798beaaa",
+                "sha256:dbeada3b8f1f6d9497840f761906c4236f912a42da4515520168bc7c525b52b0",
+                "sha256:dc77283a7c7b2b24e00fe8c3c4f7cf36bba4f65125777e906aae4d58d06d0460",
+                "sha256:deb0dd98ea4e76b833f0bfd7a6042b51115360d5dfcc7c1daa72dfc417b3327a",
+                "sha256:e039f106d48d3c241f1943bccfb383bd38ec39900d6dcaad0c73cc5fe129f346",
+                "sha256:e2654e94c705ce9b768441d8e3a387a84951ca1056efdc4a26a4a6ee723c01b6",
+                "sha256:e53419201c6c1439148feb99de6b307651a88b8defd41348cc23bbe2a290de1d",
+                "sha256:ec4a887d2236e3878c07033ad5566f6b4d5d954b85f92a219519a1745d0c93e9",
+                "sha256:ec4e87eb9916b481216b1fede7d8913be799915f5216a0c801867cbed8eeb903",
+                "sha256:ef0e6253c36e42f2637cfa3ff9b3903df60d05ec040c718999f6a0644ce1c497",
+                "sha256:ef35cef161f76031f833146f895e7e302196e01c704c00d269c04d8e18f3ac37",
+                "sha256:f7b2544eb3e7bc39ce59812371214cd97762080dab90c3afc857890039384753",
+                "sha256:f888b9565ca1d1c25ab827d184f57f4772ffbfa6baf5710b873b01936cc335ee",
+                "sha256:fa1c23ed3a02732fba906ec337df65d4cc23f9f453635e1a803c285b59c7d987",
+                "sha256:fc0a96a6828bfa6f1ccec62b54630bcdcc205d483f5a8806c0a8abb26101c54b"
+            ],
+            "index": "pypi",
+            "version": "==11.0.1"
+        },
+        "werkzeug": {
+            "hashes": [
+                "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe",
+                "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.2.3"
+        },
+        "wheel": {
+            "hashes": [
+                "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873",
+                "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.40.0"
+        },
+        "wrapt": {
+            "hashes": [
+                "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
+                "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
+                "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
+                "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
+                "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
+                "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
+                "sha256:2020f391008ef874c6d9e208b24f28e31bcb85ccff4f335f15a3251d222b92d9",
+                "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
+                "sha256:240b1686f38ae665d1b15475966fe0472f78e71b1b4903c143a842659c8e4cb9",
+                "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
+                "sha256:26046cd03936ae745a502abf44dac702a5e6880b2b01c29aea8ddf3353b68224",
+                "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
+                "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
+                "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
+                "sha256:2feecf86e1f7a86517cab34ae6c2f081fd2d0dac860cb0c0ded96d799d20b335",
+                "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
+                "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
+                "sha256:358fe87cc899c6bb0ddc185bf3dbfa4ba646f05b1b0b9b5a27c2cb92c2cea204",
+                "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
+                "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
+                "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
+                "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
+                "sha256:49ef582b7a1152ae2766557f0550a9fcbf7bbd76f43fbdc94dd3bf07cc7168be",
+                "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
+                "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
+                "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
+                "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
+                "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
+                "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
+                "sha256:6447e9f3ba72f8e2b985a1da758767698efa72723d5b59accefd716e9e8272bf",
+                "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
+                "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
+                "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
+                "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
+                "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
+                "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
+                "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
+                "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
+                "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
+                "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
+                "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
+                "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
+                "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
+                "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
+                "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
+                "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
+                "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
+                "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
+                "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
+                "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
+                "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
+                "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
+                "sha256:a9008dad07d71f68487c91e96579c8567c98ca4c3881b9b113bc7b33e9fd78b8",
+                "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
+                "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
+                "sha256:acae32e13a4153809db37405f5eba5bac5fbe2e2ba61ab227926a22901051c0a",
+                "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
+                "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
+                "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
+                "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
+                "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
+                "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
+                "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
+                "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
+                "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
+                "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
+                "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
+                "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
+                "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
+                "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
+                "sha256:ecee4132c6cd2ce5308e21672015ddfed1ff975ad0ac8d27168ea82e71413f55",
+                "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
+                "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
+                "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==1.14.1"
+        },
+        "wxpython": {
+            "hashes": [
+                "sha256:0595fdd133a552a232a99759b87891136a4500c806bbea68b888e7b6f2c9bbea",
+                "sha256:1ca327c877f276b33e2c4b6cb8417964305ee505e2509fb2000851d48b82328f",
+                "sha256:3062b4d2f5d6dcf1d59983797fc067270a5ce829a918d52a516212e798f0c9ae",
+                "sha256:3fd606d10db694c29f712f13dc3d3179d0204a71f6c1fbb5fcee859d03e9ff97",
+                "sha256:5b233c39d7bfb53b9c4928ee7c86d626f1f7a716a6dfc3acc152bb437e658751",
+                "sha256:8d846a785cd33c31e7eb42038eb159c88977d38208496b8322d14aef107f3eec",
+                "sha256:903f45131107802b38c0b5d0e964a2ce0295b67e8c3d7659821ac5b26a35658b",
+                "sha256:957a6e7cc68a8e4d7ca49c72a691b6efd5684040f4f03b112d0122e7ab470497",
+                "sha256:a15b76dad81f9bd6ceadf00fbede9aff9e09859a9aa698c53e8bd56b95d2effc",
+                "sha256:a41f3e03c3bfbe80864d7456f5c1236991fa937eedb60b986bd67e1bb47b7c3d",
+                "sha256:a7eefbc7fe7ac86479d814302711f8f118ba214229aa2a6d789fb888ee79abaa",
+                "sha256:bb7988814e706eb00792589d606d317da5661dcffbb09263cd20da956c46bcbb",
+                "sha256:c4db6d7f054d76cb1532dbd5da81b87b7a90dc9fdf18a3540063b2f8f5f3e663",
+                "sha256:e48de211a6606bf072ec3fa778771d6b746c00b7f4b970eb58728ddf56d13d5c",
+                "sha256:e87960d1963e291d4009c8547c85716b7d5325eb828cd183fdb28d441d6c3787",
+                "sha256:ff0423e84a5aaa203e7c1c3d36f8417c54cb4cd8849a43b3c917ac3bfafa4ad7"
+            ],
+            "version": "==4.2.1"
+        },
+        "xarray": {
+            "hashes": [
+                "sha256:23a404106c434215f370f642aae481339dfb91efc107533c8e9ca5f1ed8ed0f1",
+                "sha256:9f0f7e3402037c6611e802662b4374ddf55985a725bfc18dc2325cebdc06d4e7"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==2023.4.0"
+        },
+        "xmlschema": {
+            "hashes": [
+                "sha256:276a03e0fd3c94c148d528bff4d9482f9b99bf8c7b4056a2e8e703d28149d454",
+                "sha256:f2b29c45485fac414cc1fdb38d18a220c5987d7d3aa996e6df6ff35ee94d5a63"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.5.0"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31",
+                "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.17.0"
+        },
+        "zope.event": {
+            "hashes": [
+                "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26",
+                "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==5.0"
+        },
+        "zope.interface": {
+            "hashes": [
+                "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff",
+                "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c",
+                "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac",
+                "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f",
+                "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d",
+                "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309",
+                "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736",
+                "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179",
+                "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb",
+                "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941",
+                "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d",
+                "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92",
+                "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b",
+                "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41",
+                "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f",
+                "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3",
+                "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d",
+                "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8",
+                "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3",
+                "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1",
+                "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1",
+                "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40",
+                "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d",
+                "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1",
+                "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605",
+                "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7",
+                "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd",
+                "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43",
+                "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0",
+                "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b",
+                "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379",
+                "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a",
+                "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83",
+                "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56",
+                "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9",
+                "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==6.1"
+        }
+    },
+    "develop": {
+        "altgraph": {
+            "hashes": [
+                "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd",
+                "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"
+            ],
+            "version": "==0.17.3"
+        },
+        "anyio": {
+            "hashes": [
+                "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421",
+                "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"
+            ],
+            "markers": "python_full_version >= '3.6.2'",
+            "version": "==3.6.2"
+        },
+        "argcomplete": {
+            "hashes": [
+                "sha256:6c2170b3e0ab54683cb28d319b65261bde1f11388be688b68118b7d281e34c94",
+                "sha256:dc33528d96727882b576b24bc89ed038f3c6abbb6855ff9bb6be23384afff9d6"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2.0.6"
+        },
+        "astroid": {
+            "hashes": [
+                "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe",
+                "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"
+            ],
+            "markers": "python_full_version >= '3.7.2'",
+            "version": "==2.15.3"
+        },
+        "attrs": {
+            "hashes": [
+                "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
+                "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1.0"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
+                "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2022.12.7"
+        },
+        "cfgv": {
+            "hashes": [
+                "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426",
+                "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"
+            ],
+            "markers": "python_full_version >= '3.6.1'",
+            "version": "==3.3.1"
+        },
+        "charset-normalizer": {
+            "hashes": [
+                "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6",
+                "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1",
+                "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e",
+                "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373",
+                "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62",
+                "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230",
+                "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be",
+                "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c",
+                "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0",
+                "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448",
+                "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f",
+                "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649",
+                "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d",
+                "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0",
+                "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706",
+                "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a",
+                "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59",
+                "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23",
+                "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5",
+                "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb",
+                "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e",
+                "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e",
+                "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c",
+                "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28",
+                "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d",
+                "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41",
+                "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974",
+                "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce",
+                "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f",
+                "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1",
+                "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d",
+                "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8",
+                "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017",
+                "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31",
+                "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7",
+                "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8",
+                "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e",
+                "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14",
+                "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd",
+                "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d",
+                "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795",
+                "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b",
+                "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b",
+                "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b",
+                "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203",
+                "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f",
+                "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19",
+                "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1",
+                "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a",
+                "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac",
+                "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9",
+                "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0",
+                "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137",
+                "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f",
+                "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6",
+                "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5",
+                "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909",
+                "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f",
+                "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0",
+                "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324",
+                "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755",
+                "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb",
+                "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854",
+                "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c",
+                "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60",
+                "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84",
+                "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0",
+                "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b",
+                "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1",
+                "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531",
+                "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1",
+                "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11",
+                "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326",
+                "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df",
+                "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==3.1.0"
+        },
+        "colorama": {
+            "hashes": [
+                "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
+                "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
+            ],
+            "markers": "platform_system == 'Windows'",
+            "version": "==0.4.6"
+        },
+        "commitizen": {
+            "hashes": [
+                "sha256:eac18c7c65587061aac6829534907aeb208405b8230bfd35ec08503c228a7f17",
+                "sha256:fad7d37cfae361a859b713d4ac591859d5ca03137dd52de4e1bd208f7f45d5dc"
+            ],
+            "index": "pypi",
+            "version": "==2.42.1"
+        },
+        "cython": {
+            "hashes": [
+                "sha256:03daae07f8cbf797506446adae512c3dd86e7f27a62a541fa1ee254baf43e32c",
+                "sha256:0963266dad685812c1dbb758fcd4de78290e3adc7db271c8664dcde27380b13e",
+                "sha256:0ab3cbf3d62b0354631a45dc93cfcdf79098663b1c65a6033af4a452b52217a7",
+                "sha256:0e9032cd650b0cb1d2c2ef2623f5714c14d14c28d7647d589c3eeed0baf7428e",
+                "sha256:11b1b278b8edef215caaa5250ad65a10023bfa0b5a93c776552248fc6f60098d",
+                "sha256:1909688f5d7b521a60c396d20bba9e47a1b2d2784bfb085401e1e1e7d29a29a8",
+                "sha256:1d6c809e2f9ce5950bbc52a1d2352ef3d4fc56186b64cb0d50c8c5a3c1d17661",
+                "sha256:21b88200620d80cfe193d199b259cdad2b9af56f916f0f7f474b5a3631ca0caa",
+                "sha256:308c8f1e58bf5e6e8a1c4dcf8abbd2d13d0f9b1e582f4d9ae8b89857342d8bb5",
+                "sha256:44733366f1604b0c327613b6918469284878d2f5084297d10d26072fc6948d51",
+                "sha256:459994d1de0f99bb18fad9f2325f760c4b392b1324aef37bcc1cd94922dfce41",
+                "sha256:4a2723447d1334484681d5aede34184f2da66317891f94b80e693a2f96a8f1a7",
+                "sha256:56866323f1660cecb4d5ff3a1fba92a56b91b7cfae0a8253777aa4bdb3bdf9a8",
+                "sha256:5718319a01489688fdd22ddebb8e2fcbbd60be5f30de4336ea7063c3ae29fbe5",
+                "sha256:5a8de3e793a576e40ca9b4f5518610cd416273c7dc5e254115656b6e4ec70663",
+                "sha256:5c121dc185040f4333bfded68963b4529698e1b6d994da56be32c97a90c896b6",
+                "sha256:60969d38e6a456a67e7ef8ae20668eff54e32ba439d4068ccf2854a44275a30f",
+                "sha256:67b850cf46b861bc27226d31e1d87c0e69869a02f8d3cc5d5bef549764029879",
+                "sha256:742544024ddb74314e2d597accdb747ed76bd126e61fcf49940a5b5be0a8f381",
+                "sha256:7595d29eaee95633dd8060f50f0e54b27472d01587659557ebcfe39da3ea946b",
+                "sha256:7879992487d9060a61393eeefe00d299210256928dce44d887b6be313d342bac",
+                "sha256:8c3cd8bb8e880a3346f5685601004d96e0a2221e73edcaeea57ea848618b4ac6",
+                "sha256:9489de5b2044dcdfd9d6ca8242a02d560137b3c41b1f5ae1c4f6707d66d6e44d",
+                "sha256:a0f4229df10bc4545ebbeaaf96ebb706011d8b333e54ed202beb03f2bee0a50e",
+                "sha256:a8ad755f9364e720f10a36734a1c7a5ced5c679446718b589259261438a517c9",
+                "sha256:b6149f7cc5b31bccb158c5b968e5a8d374fdc629792e7b928a9b66e08b03fca5",
+                "sha256:bdb3285660e3068438791ace7dd7b1efd6b442a10b5c8d7a4f0c9d184d08c8ed",
+                "sha256:be4f6b7be75a201c290c8611c0978549c60353890204573078e865423dbe3c83",
+                "sha256:ccb223b5f0fd95d8d27561efc0c14502c0945f1a32274835831efa5d5baddfc1",
+                "sha256:cfb2302ef617d647ee590a4c0a00ba3c2da05f301dcefe7721125565d2e51351",
+                "sha256:d7ef5f68f4c5baa93349ea54a352f8716d18bee9a37f3e93eff38a5d4e9b7262",
+                "sha256:d8f822fb6ecd5d88c42136561f82960612421154fc5bf23c57103a367bb91356",
+                "sha256:dbd79221869ee9a6ccc4953b2c8838bb6ae08ab4d50ea4b60d7894f03739417b",
+                "sha256:dce0a36d163c05ae8b21200059511217d79b47baf2b7b0f926e8367bd7a3cc24",
+                "sha256:e40cf86aadc29ecd1cb6de67b0d9488705865deea4fc185c7ad56d7a6fc78703",
+                "sha256:e4401270b0dc464c23671e2e9d52a60985f988318febaf51b047190e855bbe7d",
+                "sha256:e6ef7879668214d80ea3914c17e7d4e1ebf4242e0dd4dabe95ca5ccbe75589a5",
+                "sha256:e971db8aeb12e7c0697cefafe65eefcc33ff1224ae3d8c7f83346cbc42c6c270",
+                "sha256:f674ceb5f722d364395f180fbac273072fc1a266aab924acc9cfd5afc645aae1",
+                "sha256:fd1ea21f1cebf33ae288caa0f3e9b5563a709f4df8925d53bad99be693fc0d9b"
+            ],
+            "index": "pypi",
+            "version": "==0.29.34"
+        },
+        "decli": {
+            "hashes": [
+                "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0",
+                "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.5.2"
+        },
+        "dill": {
+            "hashes": [
+                "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
+                "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==0.3.6"
+        },
+        "distlib": {
+            "hashes": [
+                "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46",
+                "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"
+            ],
+            "version": "==0.3.6"
+        },
+        "exceptiongroup": {
+            "hashes": [
+                "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+                "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==1.1.3"
+        },
+        "filelock": {
+            "hashes": [
+                "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37",
+                "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.11.0"
+        },
+        "h11": {
+            "hashes": [
+                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.14.0"
+        },
+        "httpcore": {
+            "hashes": [
+                "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599",
+                "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.17.0"
+        },
+        "httpx": {
+            "hashes": [
+                "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e",
+                "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"
+            ],
+            "index": "pypi",
+            "version": "==0.24.0"
+        },
+        "identify": {
+            "hashes": [
+                "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f",
+                "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.5.22"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.4"
+        },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb",
+                "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==6.8.0"
+        },
+        "iniconfig": {
+            "hashes": [
+                "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+                "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.0.0"
+        },
+        "isort": {
+            "hashes": [
+                "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
+                "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
+            ],
+            "markers": "python_full_version >= '3.8.0'",
+            "version": "==5.12.0"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
+                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
+            ],
+            "index": "pypi",
+            "version": "==3.1.2"
+        },
+        "lazy-object-proxy": {
+            "hashes": [
+                "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
+                "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
+                "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
+                "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
+                "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
+                "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
+                "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
+                "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
+                "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
+                "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
+                "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
+                "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
+                "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
+                "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
+                "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
+                "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
+                "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
+                "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
+                "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
+                "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
+                "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
+                "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
+                "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
+                "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
+                "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
+                "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
+                "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
+                "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
+                "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
+                "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
+                "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
+                "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
+                "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
+                "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
+                "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
+                "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.9.0"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
+                "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
+                "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
+                "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
+                "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
+                "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
+                "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
+                "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
+                "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
+                "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
+                "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
+                "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
+                "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
+                "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
+                "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
+                "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
+                "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
+                "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
+                "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
+                "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
+                "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
+                "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
+                "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
+                "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
+                "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
+                "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
+                "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
+                "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
+                "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
+                "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
+                "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
+                "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
+                "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
+                "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
+                "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
+                "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
+                "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
+                "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
+                "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
+                "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
+                "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
+                "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
+                "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
+                "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
+                "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
+                "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
+                "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
+                "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
+                "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
+                "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.7.0"
+        },
+        "nodeenv": {
+            "hashes": [
+                "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e",
+                "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+            "version": "==1.7.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
+                "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1"
+        },
+        "pefile": {
+            "hashes": [
+                "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
+                "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
+            ],
+            "markers": "sys_platform == 'win32'",
+            "version": "==2023.2.7"
+        },
+        "platformdirs": {
+            "hashes": [
+                "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08",
+                "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.2.0"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==1.0.0"
+        },
+        "pre-commit": {
+            "hashes": [
+                "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4",
+                "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"
+            ],
+            "index": "pypi",
+            "version": "==3.2.2"
+        },
+        "prompt-toolkit": {
+            "hashes": [
+                "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b",
+                "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==3.0.38"
+        },
+        "pyinstaller": {
+            "hashes": [
+                "sha256:04ecf805bde2ef25b8e3642410871e6747c22fa7254107f155b8cd179c2a13b6",
+                "sha256:05df5d2b9ca645cc6ef61d8a85451d2aabe5501997f1f50cd94306fd6bc0485d",
+                "sha256:0d167d57036219914188f1400427dd297b975707e78c32a5511191e607be920a",
+                "sha256:181856ade585b090379ae26b7017dc2c30620e36e3a804b381417a6dc3b2a82b",
+                "sha256:1b1e3b37a22fb36555d917f0c3dfb998159ff4af6d8fa7cc0074d630c6fe81ad",
+                "sha256:32727232f446aa96e394f01b0c35b3de0dc3513c6ba3e26d1ef64c57edb1e9e5",
+                "sha256:77888f52b61089caa0bee70809bbce9e9b1c613c88b6cb0742ff2a45f1511cbb",
+                "sha256:865025b6809d777bb0f66d8f8ab50cc97dc3dbe0ff09a1ef1f2fd646432714fc",
+                "sha256:d888db9afedff290d362ee296d30eb339abeba707ca1565916ce1cd5947131c3",
+                "sha256:e026adc92c60158741d0bfca27eefaa2414801f61328cb84d0c88241fe8c2087",
+                "sha256:eb083c25f711769af0898852ea30dcb727ba43990bbdf9ffbaa9c77a7bd0d720"
+            ],
+            "index": "pypi",
+            "version": "==5.6.2"
+        },
+        "pyinstaller-hooks-contrib": {
+            "hashes": [
+                "sha256:7fb856a81fd06a717188a3175caa77e902035cc067b00b583c6409c62497b23f",
+                "sha256:e02c5f0ee3d4f5814588c2128caf5036c058ba764aaf24d957bb5311ad8690ad"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2023.2"
+        },
+        "pylint": {
+            "hashes": [
+                "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8",
+                "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"
+            ],
+            "index": "pypi",
+            "version": "==2.17.2"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
+                "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
+            ],
+            "index": "pypi",
+            "version": "==7.2.0"
+        },
+        "pywin32-ctypes": {
+            "hashes": [
+                "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
+                "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
+            ],
+            "markers": "sys_platform == 'win32'",
+            "version": "==0.2.0"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==6.0.1"
+        },
+        "questionary": {
+            "hashes": [
+                "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90",
+                "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"
+            ],
+            "markers": "python_version >= '3.6' and python_version < '4.0'",
+            "version": "==1.10.0"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a",
+                "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==67.6.1"
+        },
+        "sniffio": {
+            "hashes": [
+                "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
+                "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.3.0"
+        },
+        "termcolor": {
+            "hashes": [
+                "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7",
+                "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.2.0"
+        },
+        "tomli": {
+            "hashes": [
+                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==2.0.1"
+        },
+        "tomlkit": {
+            "hashes": [
+                "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c",
+                "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.11.7"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
+                "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.5.0"
+        },
+        "virtualenv": {
+            "hashes": [
+                "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc",
+                "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==20.21.0"
+        },
+        "wcwidth": {
+            "hashes": [
+                "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e",
+                "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"
+            ],
+            "version": "==0.2.6"
+        },
+        "wrapt": {
+            "hashes": [
+                "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
+                "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
+                "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
+                "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
+                "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
+                "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
+                "sha256:2020f391008ef874c6d9e208b24f28e31bcb85ccff4f335f15a3251d222b92d9",
+                "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
+                "sha256:240b1686f38ae665d1b15475966fe0472f78e71b1b4903c143a842659c8e4cb9",
+                "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
+                "sha256:26046cd03936ae745a502abf44dac702a5e6880b2b01c29aea8ddf3353b68224",
+                "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
+                "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
+                "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
+                "sha256:2feecf86e1f7a86517cab34ae6c2f081fd2d0dac860cb0c0ded96d799d20b335",
+                "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
+                "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
+                "sha256:358fe87cc899c6bb0ddc185bf3dbfa4ba646f05b1b0b9b5a27c2cb92c2cea204",
+                "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
+                "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
+                "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
+                "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
+                "sha256:49ef582b7a1152ae2766557f0550a9fcbf7bbd76f43fbdc94dd3bf07cc7168be",
+                "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
+                "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
+                "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
+                "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
+                "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
+                "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
+                "sha256:6447e9f3ba72f8e2b985a1da758767698efa72723d5b59accefd716e9e8272bf",
+                "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
+                "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
+                "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
+                "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
+                "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
+                "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
+                "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
+                "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
+                "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
+                "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
+                "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
+                "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
+                "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
+                "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
+                "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
+                "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
+                "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
+                "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
+                "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
+                "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
+                "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
+                "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
+                "sha256:a9008dad07d71f68487c91e96579c8567c98ca4c3881b9b113bc7b33e9fd78b8",
+                "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
+                "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
+                "sha256:acae32e13a4153809db37405f5eba5bac5fbe2e2ba61ab227926a22901051c0a",
+                "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
+                "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
+                "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
+                "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
+                "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
+                "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
+                "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
+                "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
+                "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
+                "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
+                "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
+                "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
+                "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
+                "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
+                "sha256:ecee4132c6cd2ce5308e21672015ddfed1ff975ad0ac8d27168ea82e71413f55",
+                "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
+                "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
+                "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==1.14.1"
+        },
+        "yapf": {
+            "hashes": [
+                "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32",
+                "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"
+            ],
+            "index": "pypi",
+            "version": "==0.32.0"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31",
+                "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.17.0"
+        }
+    }
+}

+ 146 - 0
README.md

@@ -0,0 +1,146 @@
+# Albatross
+[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
+
+A BCI-driven system for neuro-rehabilitation
+
+## Function module
++ MI-BCI online
++ Patient and Train database
++ Online visual feedback
++ Video-based posture detection
++ EEG analysis offline and report
++ Physical feedback
+
+如图:
+
+<div align="center"> <img src="./docs/UML/overview.svg" width = 50% height = 50% /> </div>
+
+## Technology stack
++ FastAPI
++ Uvicorn as server
++ Sqlite
++ JS HTML CSS as frontend
++ flaskwebgui for desktop app
++ pyinstaller
+
+如图:
+
+<div align="center"> <img src="./docs/UML/framework.svg" width = 80% height = 80% /> </div>
+
+前后端实现细节请查看 [frontend.md](./docs/frontend.md) 和 [backend.md](./docs/backend.md)
+
+## Use case
+
+如图:
+
+<div align="center"> <img src="./docs/UML/usecase.svg" width = 50% height = 50% /> </div>
+
+
+## File structure
+
+```
+├─.git
+├─.vscode
+├─backend
+│  ├─apis                                //底层路由
+│  │  ├─general_pages
+│  │  └─version1
+│  ├─core                                //算法代码
+│  ├─db                                  //数据库相关
+│  │  ├─data                             //视频及脑电数据存放路径
+│  │  ├─models
+│  │  └─repository
+│  ├─logs
+│  ├─schemas
+│  ├─service                             //api调用到的复杂功能
+│  ├─settings                            //配置文件
+│  ├─static
+│  │  ├─config                           //配置文件资源包括前后端所需
+│  │  ├─css
+│  │  ├─images
+│  │  ├─js
+│  │  └─video
+│  ├─templates                           //页面
+│  │  ├─components
+│  │  ├─eeg
+│  │  ├─general_pages
+│  │  ├─shared
+│  │  ├─subjects
+│  │  └─trains
+│  ├─tests                               //测试api代码
+│  │  └─test_routes
+│  ├─tools                               //软件配套工具代码(离线验证工具)
+│  └─webapps                             //页面路由
+│      ├─eeg
+│      ├─subjects
+│      └─trains
+└─docs
+```
+
+## Start the project
+
+运行项目前,请先到微盘下载训练视频文件,并放到`backend/static/video` 下
+
+```bash
+# pip install pipenv
+# create a python 3.8.5 virtual environment
+# acvivate virtual env
+cd <project dir>
+pipenv install --ignore-pipfile
+# run web app
+uvicorn main:app --reload
+# visit 127.0.0.1:8000/
+# run desktop app
+python gui.py
+```
+
+除主体软件, 本项目还包含两个开发辅助工具的代码:
+
+- faker server 工具: 没有脑电硬件设备时,可使用此工具模拟信号调试
+- 离线验证工具: 用于验证在线算法的准确性
+
+### faker server的使用:
+
+通过以下命令
+```bash
+cd backend
+python -m core.sig_chain.device.fake_sig.sig_fake_server
+```
+启动 faker server (或运行[提前打包](#faker-server-工具)好的软件使用), 然后在配置文件中修改设备为 faker,最后启动albatross。
+
+
+### 离线验证工具的使用
+
+参考tools下的[README.md](./backend/tools/README.md)
+
+
+## 打包
+
+使用 [pyinstaller](https://pyinstaller.org/en/stable/index.html) 打包
+
+### 主体软件
+
+```
+python build_pyd.py build_ext --inplace
+pyinstaller -y albatross.spec
+```
+
+打包完在 `backend/dist` 下会发现 albatross 文件夹,即为打包好的应用。
+
+
+### faker server 工具
+
+[](#faker_server)
+
+执行 `backend/core/sig_chain/device/fake_sig` 下的脚本
+
+```
+faker-server-setup.ps1
+```
+
+生成的exe文件在同级目录下的 `dist` 文件夹中
+
+
+### 离线验证工具
+
+参考backend下的[process_offline.md](./backend/process_offline.md)

+ 8 - 0
backend/.streamlit/config.toml

@@ -0,0 +1,8 @@
+[general]
+email = ""
+
+[server]
+maxUploadSize = 3000
+
+[client]
+toolbarMode = "minimal"

+ 2 - 0
backend/.streamlit/secrets.toml

@@ -0,0 +1,2 @@
+[connections.sql_app]
+url = "sqlite:///sql_app.db"

+ 386 - 0
backend/apis/version1/route_eeg.py

@@ -0,0 +1,386 @@
+"""module apis/version1/route_eeg provide backend apis"""
+import asyncio
+import logging
+
+
+from fastapi import APIRouter
+from fastapi import Depends
+from fastapi import HTTPException, status
+from fastapi import WebSocket
+from func_timeout import FunctionTimedOut
+import numpy as np
+# from scipy import signal
+from sqlalchemy.orm import Session
+
+from core import utils
+from core.mi.eeg_csp import CSPBasedClassifier
+from core.mi.eeg_psd import PSDBasedClassifier
+from core.mi.utils import SelectedCspChannel
+from core.mi.utils import SelectedPsdChannel
+from core.mi.pipeline import BaselineModel
+from core.sig_chain.device.connector_interface import DataMode
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.pre_process import PreProcessor
+from core.sig_chain.pre_process import RealTimeFilterM
+from core.sig_chain.sig_receive import Receiver
+from db.models.trains import Limbs
+from db.models.trains import TrainStatus
+from db.repository import subjects as db_rep_sub
+from db.repository import trains as db_rep_train
+from db.session import get_db
+from service import eeg as es
+from settings.config import settings
+
+logger = logging.getLogger(__name__)
+router = APIRouter()
+
+csp_dc = es.CSPDataCollector()
+psd_dc = es.PSDDataCollector(
+    maxlen=int(settings.TRAIN_PARAMS['rest_stim_duration'] / 1000))
+psd_clf = PSDBasedClassifier()
+csp_clf = CSPBasedClassifier()
+train_finish_flag = False
+pipeline = BaselineModel("static/models/bp-baseline.pkl")
+
+
+@router.get("/train-configs")
+def get_train_configs():
+    return settings.TRAIN_PARAMS
+
+
+@router.get("/eeg-edf-set-header")
+def eeg_edf_set_header(subject_id: str = None,
+                       train_id: int = None,
+                       task_per_run: int = None,
+                       db: Session = Depends(get_db)):
+    """创建BDF数据的数据头
+
+    Args:
+        subject_id (str, optional): 患者ID. Defaults to None.
+        train_id (int, optional): 训练ID. Defaults to None.
+        db (Session, optional): 数据库. Defaults to Depends(get_db).
+
+    Returns:
+        1: 创建成功
+    """
+    path = utils.create_data_dir(subject_id, train_id)
+    subject = db_rep_sub.retrieve_subject_by_id(id=subject_id, db=db)
+    train = db_rep_train.retrieve_train(id=train_id, db=db)
+
+    position_name = "test"
+    if Limbs.get_item_name(train.position) is not None:
+        position_name = Limbs.get_item_name(train.position).lower()
+    # pylint: disable=line-too-long
+    filename = f"{subject.id_card}_{train.start_time.strftime('%Y%m%d%H%M%S')}_{position_name}.bdf"
+    # pylint: enable=line-too-long
+
+    receiver = Receiver()
+    receiver.connector.set_saver()
+    receiver.connector.saver.set_edf_header(subject, filename, task_per_run,
+                                            path)
+    update_dict = {"train_status": TrainStatus.TRAINING}
+    db_rep_train.partial_update_train_by_id(train_id, update_dict, db)
+    return 1
+
+
+@router.get("/eeg-edf-mark")
+def eeg_edf_mark(time_seconds: int, mark: str):
+    """数据打标签
+
+    Args:
+        time_seconds (int): 打标签的时间点
+        mark (str): 标记
+
+    Returns:
+        1: 成功
+    """
+    receiver = Receiver()
+    receiver.connector.saver.edf_data_mark(time_seconds, mark)
+    return 1
+
+
+@router.get("/eeg-device-connect")
+def eeg_device_connect():
+    """脑电设备连接
+    """
+    device = settings.config["test_parameter"]["device"]
+    receiver = Receiver()
+    if device == "faker":
+        config_info = settings.config.get("faker_eeg_config")
+        receiver.select_connector(Device.FAKER,
+                                config_info.get("buffer_plot_size_seconds"),
+                                config_info)
+    elif device == "pony":
+        config_info = settings.config.get("pony_eeg_config")
+        receiver.select_connector(Device.PONY,
+                                config_info.get("buffer_plot_size_seconds"),
+                                config_info)
+    elif device == "neo":
+        config_info = settings.config.get("neo_eeg_config")
+        receiver.select_connector(Device.NEO,
+                                config_info.get("buffer_plot_size_seconds"),
+                                config_info)
+    else:
+        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+                            detail="Invalid device name")
+    psd_clf.update_params(receiver.connector.sample_params.sample_rate)
+
+    success = receiver.setup_connector()
+    if success:
+        return {"msg": success}
+    else:
+        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+                            detail="EEG device connected failed")
+
+
+# 在get_wave_from_buffer直接获取数据, 后续考虑删除
+@router.get("/data-buffer")
+def start_receive_wave():
+    """获取数据到buffer
+    """
+    receiver = Receiver()
+    try:
+        receiver.start_receive_wave()
+    except AssertionError as exc:
+        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+                            detail="Start receive wave failed") from exc
+    return {"msg": "success"}
+
+
+@router.websocket("/data")
+# pylint: disable=redundant-returns-doc
+# pylint: disable=missing-raises-doc
+async def get_wave_from_buffer(websocket: WebSocket):
+    # pylint: enable=redundant-returns-doc
+    # pylint: enable=missing-raises-doc
+    """获取脑电数据
+
+    Returns:
+        JSON: 返回的脑电数据,(通道数*采样率)
+        raises HTTPException: status状态错误(500)
+        raises HTTPException: status状态错误(408)
+    """
+    receiver = Receiver()
+    filter_m_high = RealTimeFilterM.init_eeg(
+        0, receiver.connector.sample_params.channel_count)
+    await websocket.accept()
+    while True:
+        # 时间参数要比plot buffer小, 可以考虑以plot buffer的二分之一设置
+        await asyncio.sleep(receiver.buffer_plot.package_len / 2)
+        try:
+            # await websocket.receive_text()
+            ret = None
+            timestamp = None
+            receiver.connector.receive_wave()
+            data_from_buffer = receiver.get_data_from_buffer("plot")
+            if data_from_buffer["status"] == "ok":
+                raw_waves = data_from_buffer["data"]
+                timestamp = data_from_buffer["timestamp"]
+                #TODO:  预处理的相关参数设置
+                resampled_waves = PreProcessor.resample_direct(
+                    raw_waves, settings.config["frontend_plot"]["sample_rate"])
+                _, samples = resampled_waves.get_data().shape
+                m_yy = np.zeros_like(resampled_waves.get_data(),
+                                     dtype=np.float64)
+                for ii in range(samples):
+                    xn = resampled_waves.get_data()[:, ii]
+                    m_yy[:, ii] = filter_m_high.filter(xn)
+
+                ret = m_yy.tolist()
+                # await websocket.send_json(ret)
+                # ret = raw_waves.get_data().tolist()
+        except RuntimeError as exc:
+            raise HTTPException(
+                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc
+        except FunctionTimedOut as exc:
+            raise HTTPException(
+                status_code=status.HTTP_408_REQUEST_TIMEOUT) from exc
+        await websocket.send_json({"timestamp": timestamp, "eegdata": ret})
+
+
+@router.get("/wave-mode-connect")
+def wave_mode_connect():
+    """阻抗模式连接
+    """
+    receiver = Receiver()
+    receiver.setup_receive_mode(DataMode.WAVE)
+
+
+@router.get("/impedance-model-connect")
+def impedance_mode_connect():
+    """阻抗模式连接
+    """
+    receiver = Receiver()
+    receiver.setup_receive_mode(DataMode.IMPEDANCE)
+
+
+@router.get("/impedance-data")
+# pylint: disable=missing-raises-doc
+def get_impedance():
+    # pylint: enable=missing-raises-doc
+    """阻抗数据获取
+
+    Returns:
+        JSON: 阻抗数据
+
+    Raises: HTTPException(503)
+    """
+    receiver = Receiver()
+    try:
+        impedance = receiver.receive_impedance()
+    except AssertionError as exc:
+        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+                            detail="Receive impedance failed") from exc
+    return {"impedance": impedance}
+
+
+@router.get("/eeg-model-close")
+def eeg_mode_close():
+    """脑电数据模式关闭
+    """
+    receiver = Receiver()
+    receiver.stop_receive()
+
+# TODO: 两个close 合并
+@router.get("/impedance-model-close")
+def impedance_mode_close():
+    """阻抗模式关闭
+    """
+    receiver = Receiver()
+    receiver.stop_receive()
+
+
+@router.get("/initial-rest-state-run")
+def initial_rest_state_run(position: str, duration: int):
+    """训练最开始的静息处理
+
+    Args:
+        position (str): 训练部位
+        duration (int): 静息态持续时间
+    """
+    # logger.debug("训练部位:%s", position)
+    receiver = Receiver()
+
+    # TODO: 放到reset部分?
+    csp_dc.set_collected_channel(
+        SelectedCspChannel(receiver.connector.device).get_channel_ids(
+            position, receiver.connector.sample_params.channel_labels))
+    psd_dc.set_collected_channel(
+        SelectedPsdChannel(receiver.connector.device).get_channel_ids(
+            position, receiver.connector.sample_params.channel_labels))
+
+    train_success = es.initial_rest_process(receiver, psd_dc, duration, psd_clf)
+    return {"train_success": train_success}
+
+
+@router.get("/mi-state-run")
+def mi_state_run(current_round: int, duration: int, sample_duration:int):
+    """一个任务的mi过程处理
+
+    Args:
+        current_round (int): 当前轮数.
+        duration (int): 运动想象是时长(second).
+        sample_duration (int): 用于训练CSP的数据样本长度(second).
+
+    Returns:
+        list: predicts, 分类结果, 1是运动想象, 0是静息
+    """
+    assert duration >= sample_duration, \
+        "Duration >= sample_duration not satisfied!"
+
+    receiver = Receiver()
+    predicts = es.one_task_mi_process(receiver, psd_dc, csp_dc, current_round,
+                                      duration, sample_duration, psd_clf,
+                                      csp_clf)
+    return {"predicts": predicts}
+
+
+@router.get("/rest-state-run")
+def rest_state_run(tasks_per_round: int, duration: int, sample_duration: int):
+    """一个任务的rest过程处理
+
+    Args:
+        tasks_per_round (int): 每轮的任务个数.
+        duration (int): 休息时长(second).
+        sample_duration (int): 用于训练CSP的数据样本长度(second).
+
+    Returns:
+        _type_: _description_
+    """
+    assert duration >= sample_duration, "Duration >= sample_duration not satisfied!"
+    receiver = Receiver()
+    es.one_task_rest_process(receiver, csp_dc, tasks_per_round, duration,
+                             sample_duration, csp_clf)
+
+    return {"success"}
+
+
+@router.get("/mi-test-run")
+def mi_test_run(current_round: int):
+    """一个任务的mi过程处理
+
+    Args:
+        current_round (int): 当前轮数.
+
+    Returns:
+        list: predicts, 分类结果, 1是运动想象, 0是静息
+    """
+
+    receiver = Receiver()
+
+    receiver.connector.receive_wave()
+    data_from_buffer = receiver.get_data_from_buffer("classify_online")
+    if data_from_buffer["status"] == "ok":
+        predict = pipeline.smoothed_decision(data_from_buffer)
+        timestamps = data_from_buffer["timestamp"]
+        receiver.connector.saver.edf_data_mark(timestamps[0], str(predict))
+    else:
+        predict = None
+
+    return {"predict": predict}
+
+
+@router.get("/eeg-pipeline-reset")
+def eeg_pipeline_reset():
+    """每次判成功后reset pipeline buffer
+    """
+    pipeline.reset_buffer()
+
+
+@router.get("/eeg-clf-reset")
+def eeg_clf_reset():
+    """开始训练时要重置参数
+    """
+    global csp_clf
+    global psd_clf
+    global csp_dc
+    global psd_dc
+
+    csp_clf = CSPBasedClassifier()
+
+    receiver = Receiver()
+    psd_clf = PSDBasedClassifier(receiver.connector.sample_params.sample_rate)
+
+    csp_dc = es.CSPDataCollector()
+    psd_dc = es.PSDDataCollector(
+        maxlen=int(settings.TRAIN_PARAMS['rest_stim_duration'] / 1000))
+
+
+@router.get("/set-train-finish-flag")
+def set_train_finish_flag(flag):
+    global train_finish_flag
+    train_finish_flag = flag
+    return "设置成功"
+
+
+@router.get("/get-train-finish-flag")
+def get_train_finish_flag():
+    return {"train_finish_flag": train_finish_flag}
+
+
+@router.get("/restart-fake-data")
+def restart_fake_data():
+    receiver = Receiver()
+    if receiver.connector.device == Device.FAKER:
+        receiver.reset_wave()
+    return {"status": 1}

+ 76 - 0
backend/apis/version1/route_peripheral.py

@@ -0,0 +1,76 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   route_peripheral.py
+@Time    :   2023/03/29 16:14:13
+'''
+
+from fastapi import APIRouter
+from fastapi import Body
+from fastapi import Depends
+from starlette.responses import JSONResponse
+from sqlalchemy.orm import Session
+
+from core.peripheral.manager import PeripheralHandManager
+from core.peripheral.hand.fubo_pneumatic_finger import get_serial_ports
+from db.repository import trains as db_rep_train
+from db.session import get_db
+from settings.config import settings
+
+language = settings.config["lang"]
+message_dict = settings.get_message()
+message = message_dict[language]
+router = APIRouter()
+
+hand_manager = None
+
+
+@router.get("/serial-ports")
+def get_ports():
+    available_ports = get_serial_ports()
+    return JSONResponse({"available_ports": available_ports})
+
+
+@router.post("/hand/init")
+def hand_init(device_name: str = Body(...), init_params: dict = Body(...)):
+    global hand_manager
+    if hand_manager is not None:
+        hand_manager.close()
+    hand_manager = PeripheralHandManager(device_name, init_params)
+    return JSONResponse(hand_manager.init())
+
+
+@router.get("/hand/start")
+def hand_start(train_id: int, db: Session = Depends(get_db)):
+    train = db_rep_train.retrieve_train(train_id, db)
+    if hand_manager is None:
+        return JSONResponse({"msg": message["hand_peripheral_not_init"]})
+    msg = hand_manager.start(train)
+    return JSONResponse(msg)
+
+
+@router.get("/hand/stop")
+def hand_stop():
+    if hand_manager is None:
+        return JSONResponse({"msg": message["hand_peripheral_not_init"]})
+    msg = hand_manager.stop()
+    return JSONResponse(msg)
+
+
+@router.get("/hand/status")
+def hand_status():
+    if hand_manager is None:
+        return JSONResponse({"is_connected": False, "msg": message["hand_peripheral_not_init"]})
+    msg = hand_manager.status()
+    return JSONResponse(msg)
+
+
+@router.get("/hand/close")
+def hand_close():
+    global hand_manager
+    if hand_manager is None:
+        return JSONResponse({"msg": message["hand_peripheral_not_init"]})
+    msg = hand_manager.close()
+    del hand_manager
+    hand_manager = None
+    return JSONResponse(msg)

+ 11 - 0
backend/components/remove_style.py

@@ -0,0 +1,11 @@
+"""remove some streamlit style"""
+import streamlit as st
+
+
+def hide_footer():
+    hide_st_style = """
+                        <style>
+                        footer {visibility: hidden;}
+                        </style>
+                    """
+    st.markdown(hide_st_style, unsafe_allow_html=True)

+ 0 - 0
backend/core/__init__.py


+ 16 - 0
backend/core/mi/feature_extractors.py

@@ -0,0 +1,16 @@
+from mne.time_frequency import tfr_array_morlet
+
+
+def filterbank_extractor(data, sfreq, filter_banks, reshape_freqs_dim=False):
+    n_cycles = filter_banks / 4
+    power = tfr_array_morlet(data[None],
+                            sfreq=sfreq,
+                            freqs=filter_banks,
+                            n_cycles=n_cycles,
+                            output='avg_power',
+                            verbose=False)
+    # (n_ch, n_freqs, n_times)
+    # remove power line noise, * f to normalize
+    if reshape_freqs_dim:
+        power = power.reshape((-1, power.shape[-1]))
+    return power

+ 40 - 0
backend/core/mi/model.py

@@ -0,0 +1,40 @@
+import numpy as np
+
+from sklearn.linear_model import LogisticRegression
+from sklearn.pipeline import make_pipeline
+from sklearn.base import BaseEstimator, TransformerMixin
+from sklearn.preprocessing import StandardScaler
+
+from mne.decoding import Vectorizer
+
+
+class ChannelScaler(BaseEstimator, TransformerMixin):
+    def __init__(self, norm_axis=(0, 2)):
+        self.channel_mean_ = None
+        self.channel_std_ = None
+        self.norm_axis=norm_axis
+
+    def fit(self, X, y=None):
+        '''
+
+        :param X: 3d array with shape (n_epochs, n_channels, n_times)
+        :param y:
+        :return:
+        '''
+        self.channel_mean_ = np.mean(X, axis=self.norm_axis, keepdims=True)
+        self.channel_std_ = np.std(X, axis=self.norm_axis, keepdims=True)
+        return self
+
+    def transform(self, X, y=None):
+        X = X.copy()
+        X -= self.channel_mean_
+        X /= self.channel_std_
+        return X
+
+
+def baseline_model(C=1.):
+    return make_pipeline(
+        Vectorizer(),
+        StandardScaler(),
+        LogisticRegression(C=C)
+    )

+ 47 - 0
backend/core/mi/pipeline.py

@@ -0,0 +1,47 @@
+import joblib
+import numpy as np
+from scipy import signal
+from core.mi.feature_extractors import filterbank_extractor
+
+
+class BaselineModel:
+    def __init__(self, model_path, buffer_steps=5):
+        self.model = joblib.load(model_path)
+        self._freqs = np.arange(20, 150, 15)
+        self.buffer_steps = buffer_steps
+        self.buffer = []
+    
+    def reset_buffer(self):
+        self.buffer = []
+    
+    def step_probability(self, fs, data):
+        # TODO: make sure if scaling is needed
+        # data *= 0.0298 * 1e-6
+        # filter data
+        filter_bank_data = filterbank_extractor(data, fs, self._freqs, reshape_freqs_dim=True)
+        # downsampling
+        filter_bank_data = signal.decimate(filter_bank_data, 10, axis=-1, zero_phase=True)
+        filter_bank_data = signal.decimate(filter_bank_data, 10, axis=-1, zero_phase=True)
+        # predict proba
+        p = self.model.predict_proba(filter_bank_data[None]).squeeze()
+        return p[1]
+    
+    def _parse_data(self, data):
+        data = data['data']
+        fs = data.info['sfreq']
+        data = data.get_data()
+        # drop last event channel
+        data = data[:-1]
+        return fs, data
+    
+    def smoothed_decision(self, data):
+        """
+            Interface for class decision
+        """
+        fs, data = self._parse_data(data)
+        p = self.step_probability(fs, data)
+        self.buffer.append(p)
+        if len(self.buffer) > self.buffer_steps:
+            self.buffer.pop(0)
+        aveg_p = np.mean(self.buffer)
+        return int(aveg_p > 0.9)

+ 117 - 0
backend/core/mi/utils.py

@@ -0,0 +1,117 @@
+"""想象运动工具类
+"""
+from enum import Enum
+
+from core.sig_chain.device.connector_interface import Device
+
+
+class Mark(Enum):
+    REST = "rest"
+    MI = "mi"
+
+
+class SelectedChannel():
+
+    def get_channel_names(self, position:str):
+        if position == "左手":
+            return self.left_hand_channel
+        elif position == "右手":
+            return self.right_hand_channel
+        else:
+            return self.foot_channel
+
+    def get_channel_ids(self, position:str, channel_labels: list):
+        selected = []
+        if position == "左手":
+            selected = self.left_hand_channel
+        elif position == "右手":
+            selected = self.right_hand_channel
+        else:
+            selected = self.foot_channel
+        try:
+            selected_ids = [channel_labels.index(item) for item in selected]
+            return selected_ids
+        except ValueError as exc:
+            # pylint: disable=line-too-long
+            raise Exception(
+                f"Some selected channel({selected}) missing in input channel({channel_labels})"
+            ) from exc
+            # pylint: enable=line-too-long
+
+
+class SelectedCspChannel(SelectedChannel):
+
+    def __init__(self, device: Device):
+        if device == Device.NEO:
+            self.left_hand_channel = ["C4", "FC4", "CP2", "CP6"]
+            self.right_hand_channel = ["C3", "FC3", "CP5", "CP1"]
+        else:
+            self.left_hand_channel = [
+                "Fz", "F4", "F8", "Cz", "C4", "T4", "Pz", "P4", "T6"
+            ]
+            self.right_hand_channel = [
+                "F7", "F3", "Fz", "T3", "C3", "Cz", "T5", "P3", "Pz"
+            ]
+            self.foot_channel = [
+                "F3", "Fz", "F4", "C3", "Cz", "C4", "P3", "Pz", "P4"
+            ]
+
+
+class SelectedCspPlotChannel(SelectedChannel):
+
+    def __init__(self, device: Device):
+        if device == Device.NEO:
+            self.left_hand_channel = [
+                "C3", "FC3", "CP5", "CP1", "C4", "FC4", "CP2", "CP6"
+            ]
+            self.right_hand_channel = self.left_hand_channel
+        else:
+            self.left_hand_channel = [
+                "T6", "P4", "Pz", "F8", "F4", "Fp1", "Cz", "F7", "F3", "C3",
+                "T3", "Oz", "O1", "O2", "Fz", "C4", "T4", "Fp2", "T5", "P3"
+            ]
+            self.right_hand_channel = self.left_hand_channel
+            self.foot_channel = self.left_hand_channel
+
+
+class SelectedPsdChannel(SelectedChannel):
+
+    def __init__(self, device: Device):
+        self.left_hand_channel = ["C4"]
+        self.right_hand_channel = ["C3"]
+        if device != Device.NEO:
+            self.foot_channel = ["Cz"]
+
+
+class SelectedPsdPlotChannel(SelectedChannel):
+
+    def __init__(self, device: Device):
+        channels = ["C3","C4"]
+        self.left_hand_channel = channels
+        self.right_hand_channel = channels
+        if device != Device.NEO:
+            self.foot_channel = ["C3", "Cz", "C4"]
+
+
+class SelectedErdsChannel(SelectedChannel):
+    def __init__(self, device: Device):
+        self.left_hand_channel = ["C3", "C4"]
+        self.right_hand_channel = ["C3", "C4"]
+        if device != Device.NEO:
+            self.foot_channel = ["C3", "Cz", "C4"]
+
+
+class SelectedWpliChannel(SelectedChannel):
+
+    def __init__(self, device: Device):
+        if device == Device.NEO:
+            self.left_hand_channel = [
+                "C3", "FC3", "CP5", "CP1", "C4", "FC4", "CP2", "CP6"
+            ]
+        else:
+            self.left_hand_channel = [
+                "T6", "P4", "Pz", "F8", "F4", "Fp1", "Cz", "F7", "F3", "C3",
+                "T3", "Oz", "O1", "O2", "Fz", "C4", "T4", "Fp2", "T5", "P3"
+            ]
+        self.right_hand_channel = self.left_hand_channel
+        self.foot_channel = self.left_hand_channel

+ 0 - 0
backend/core/peripheral/__init__.py


+ 21 - 0
backend/core/peripheral/factory.py

@@ -0,0 +1,21 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   peripheral_factory.py
+@Time    :   2023/03/29 10:14:50
+'''
+
+from core.peripheral.hand.ruishou import RuishouClient
+from core.peripheral.hand.fubo_pneumatic_finger import FuboPneumaticFingerClient
+from core.peripheral.hand.fubo_mechanical_finger import FuboMechanicalFingerClient
+
+
+class PeripheralHandFactory():
+
+    def create_client(self, name, init_params=None):
+        if name == "ruishou":
+            return RuishouClient()
+        if name == "fubo_pneumatic_finger":
+            return FuboPneumaticFingerClient(init_params)
+        if name == "fubo_mechanical_finger":
+            return FuboMechanicalFingerClient(init_params)

+ 37 - 0
backend/core/peripheral/hand/base.py

@@ -0,0 +1,37 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   base.py
+@Time    :   2023/03/28 16:47:23
+'''
+
+from abc import ABC, abstractmethod
+
+
+class PeripheralHandBase(ABC):
+    """机械手外设抽象类"""
+
+    @abstractmethod
+    def init(self):
+        """初始化"""
+        pass
+
+    @abstractmethod
+    def start(self):
+        """设备操作启动"""
+        pass
+
+    @abstractmethod
+    def stop(self):
+        """设备操作立即停止"""
+        pass
+
+    @abstractmethod
+    def status(self):
+        """返回设备状态"""
+        pass
+
+    @abstractmethod
+    def close(self):
+        """关闭设备连接"""
+        pass

+ 289 - 0
backend/core/peripheral/hand/fubo_mechanical_finger.py

@@ -0,0 +1,289 @@
+'''
+@Author  :   gongchanghui
+@Ide     :   vscode
+@File    :   fubo_mechanical_finger.py
+@Time    :   2023/08/29 16:49:11
+'''
+
+
+import logging
+import time
+import enum
+import threading
+import serial
+from serial.tools.list_ports import comports
+
+from core.peripheral.hand.base import PeripheralHandBase
+from settings.config import settings
+
+mechanical_finger_config = settings.config["hand_peripheral_parameter"]
+logger = logging.getLogger(__name__)
+
+
+def get_serial_ports():
+    """获取可选端口"""
+    ports = list(comports(include_links=False))
+    available_ports_list = [port.device for port in ports]
+    return available_ports_list
+
+class DeviceStatus(enum.Enum):
+    NotOpend = 0
+    Opened = 1
+    DeviceRecieved = 2
+    RMTC_GLOVE_Sended=3
+    Get_Recieved=4
+    Running=5
+    Invalid=-1
+
+class RecievedPackageType(enum.Enum):
+    Device_Package = 0
+    Get_Package = 1
+    Other_Package = 2
+    No_Package = 3
+
+
+class FuboMechanicalFingerConnector:
+    """富伯客户端
+
+    功能:连接;发送持续控制包维持链接;开启线程发送信号;接收信号等
+    """
+
+    def __init__(self, port) -> None:
+        self.__port = port
+        self.__heart_interval = 0.1
+        self.__serial = None
+        self.__baud_rate = 57600
+        self.__data_bite = 8
+        self.__timeout = 1
+        self.__stop_bit = serial.STOPBITS_ONE  # 停止位
+        self.__parity_bit = serial.PARITY_NONE  # 校验位
+        self.__extend = False
+        self.__extend_target_time = time.time()-1
+
+        self.__thumb = 60
+        self.__index_finger = 60
+        self.__middle_finger = 60
+        self.__ring_finger = 60
+        self.__little_finger = 60
+        self.__duration = 10
+
+        self.is_connected = False
+        self.stop_flag = False
+
+    def connect(self):
+        if self.__serial is not None:
+            self.__serial.close()
+        self.__serial = serial.Serial(port=self.__port,
+                                      baudrate=self.__baud_rate,
+                                      parity=self.__parity_bit,
+                                      stopbits=self.__stop_bit,
+                                      timeout=self.__timeout)
+        if self.__serial.is_open:
+            logger.info("Open Fubo Mechanical Finger Device Failed...")
+        else:
+            logger.warning("Open Fubo Mechanical Finger Device Failed...")
+
+        return self.__serial.is_open
+
+    def start_client(self):
+        """启动客户端"""
+
+        try:
+            self.is_connected = self.connect()
+        except Exception:
+            return False
+        self.stop_flag = False
+        if not self.is_connected:
+            return False
+        self.__extend_target_time = time.time()+2
+        # 向服务端发送心跳包
+        working_thread = threading.Thread(target=self.__working_thrend_func,
+                                            args=())
+        working_thread.start()
+        return True
+
+    def create_send_t(self, send_data):
+        """外部程序调用"""
+        send_t = threading.Thread(target=self.send_data, args=(send_data,))
+        send_t.start()
+        send_t.join()
+
+    def send_data(self, cmd, update=None):
+        if update is None:
+            update = dict()
+        send_data = self.protocol.get_pck(cmd, update)
+        self.sock.sendall(send_data)
+
+    def sync_send_data(self, cmd, update=None):
+        """同步接口"""
+        if update is None:
+            update = dict()
+        send_data = self.protocol.get_pck(cmd, update)
+        if send_data:
+            self.sock.send(send_data)
+            res = self.filter_recv_msg()
+            return res
+        else:
+            return None
+
+    def extend(self, thumb, index_finger, middle_finger, ring_finger, little_finger, duration):
+        self.__extend_target_time = time.time()+int(duration)
+        self.__thumb = int(thumb)
+        self.__index_finger = index_finger
+        self.__middle_finger = middle_finger
+        self.__ring_finger = ring_finger
+        self.__little_finger = little_finger
+        self.__duration = duration
+
+    def flex(self):
+        self.__extend_target_time = time.time()-1
+
+    def __working_thrend_func(self):
+        device_status = DeviceStatus.Opened
+        last_device_status = device_status
+        command_count = 0
+        time_last_receive_package = time.time()
+        while self.stop_flag is not True:
+            try:
+                if not self.__serial.is_open:
+                    self.is_connected = self.connect()
+
+                data_count_in_buffer = self.__serial.in_waiting
+                in_data = None
+                in_data_str = None
+                package_type = RecievedPackageType.No_Package
+
+                if data_count_in_buffer > 0:
+                    now = time.time()
+                    if now - time_last_receive_package > 3:
+                        device_status = DeviceStatus.Opened
+
+                    in_data = self.__serial.read(data_count_in_buffer)
+                    in_data_str = str(in_data, encoding="utf-8")
+                    if data_count_in_buffer >= 3 and "get" in in_data_str:
+                        package_type = RecievedPackageType.Get_Package
+                        time_last_receive_package = now
+                    elif data_count_in_buffer >= 5 and "device" in in_data_str:
+                        package_type = RecievedPackageType.Device_Package
+                        time_last_receive_package = now
+                        self.__extend_target_time = time.time()+2
+                    else:
+                        package_type = RecievedPackageType.Other_Package
+
+                    if last_device_status != device_status:
+                        print(device_status)
+                        last_device_status = device_status
+                    #if data_count_in_buffer > 0:
+                        #print(device_status)
+                        #print_hex("recv data:", in_data)
+
+                    if device_status is DeviceStatus.NotOpend:
+                        pass
+                    elif device_status is DeviceStatus.Opened:
+                        if data_count_in_buffer <= 0:
+                            continue
+
+                        if package_type is RecievedPackageType.Device_Package:
+                            print("**** device command recieved***")
+                            device_status = DeviceStatus.DeviceRecieved
+                    elif device_status is DeviceStatus.DeviceRecieved:
+                        self.__serial.write(b"RMTC_GLOVE\r")
+                        device_status = DeviceStatus.RMTC_GLOVE_Sended
+                        pass
+                    elif device_status is DeviceStatus.RMTC_GLOVE_Sended:
+                        if data_count_in_buffer <= 0:
+                            continue
+
+                        if package_type is RecievedPackageType.Get_Package:
+                            print("**** get recieved***")
+                            device_status = DeviceStatus.Get_Recieved
+                    elif device_status is DeviceStatus.Get_Recieved:
+                        self.__serial.write(b"DATA:0:40:0:40:0:40:0:40:0:40\r")
+                        device_status = DeviceStatus.Running
+                    elif device_status is DeviceStatus.Running:
+                        if now > self.__extend_target_time:
+                            self.__serial.write(b"DATA:0:00:0:00:0:00:0:00:0:00\r")
+                        else:
+                            cmd_str = "DATA:0:{0}:0:{1}:0:{2}:0:{3}:0:{4}\r".format(self.__thumb,
+                                                                                    self.__index_finger,
+                                                                                    self.__middle_finger,
+                                                                                    self.__ring_finger,
+                                                                                    self.__little_finger)
+                            self.__serial.write(cmd_str.encode('UTF-8'))
+                    elif device_status is DeviceStatus.Invalid:
+                        pass
+
+                    if package_type is RecievedPackageType.Device_Package and \
+                            device_status is not DeviceStatus.Opened and \
+                            device_status is not DeviceStatus.DeviceRecieved and \
+                            device_status is not DeviceStatus.RMTC_GLOVE_Sended:
+                        print("**** device command recieved***")
+                        device_status = DeviceStatus.DeviceRecieved
+            except Exception as e:
+                logger.info(
+                    "Send beat data failed, Hand Peripheral may be disconnected.")
+                self.is_connected = False
+            time.sleep(self.__heart_interval)
+        if self.__serial is not None and self.__serial.is_open:
+            self.__serial.close()
+        self.__serial = None
+
+    def close_client(self):
+        self.stop_flag = True
+
+
+class FuboMechanicalFingerClient(PeripheralHandBase):
+    """富伯机械手客户端"""
+
+    def __init__(self, init_params=None):
+        if init_params:
+            self.port = init_params["port"]
+            self.__thumb = int(init_params["thumb"])
+            self.__index_finger = int(init_params["index_finger"])
+            self.__middle_finger = int(init_params["middle_finger"])
+            self.__ring_finger = int(init_params["ring_finger"])
+            self.__little_finger = int(init_params["little_finger"])
+            self.__duration = int(init_params["duration"])
+        else:
+            self.port = "COM12"
+            self.__thumb = 60
+            self.__index_finger = 60
+            self.__middle_finger = 60
+            self.__ring_finger = 60
+            self.__little_finger = 60
+            self.__duration = 10
+
+        self.connector = FuboMechanicalFingerConnector(self.port)
+
+    def __del__(self):
+        if self.connector is not None:
+            self.connector.close_client()
+        self.connector = None
+
+    def init(self):
+
+        ret = self.connector.start_client()
+        return {"is_connected": self.connector.is_connected, "msg": ret}
+
+    def start(self, train=None):
+        self.connector.extend(self.__thumb,
+                              self.__index_finger,
+                              self.__middle_finger,
+                              self.__ring_finger,
+                              self.__little_finger,
+                              self.__duration)
+        return 1
+
+    def stop(self):
+        self.connector.flex()
+        return 1
+
+    def status(self):
+        status = {"is_connected": self.connector.is_connected}
+        return status
+
+    def close(self):
+        if self.connector:
+            self.connector.close_client()
+        self.connector = None
+        return {"is_connected": False}

+ 115 - 0
backend/core/peripheral/hand/fubo_pneumatic_finger.py

@@ -0,0 +1,115 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   fubo_pneumatic_finger.py
+@Time    :   2023/04/03 16:49:11
+'''
+
+
+import logging
+import time
+
+import serial
+from serial.tools.list_ports import comports
+
+from core.peripheral.hand.base import PeripheralHandBase
+
+logger = logging.getLogger(__name__)
+
+
+def get_serial_ports():
+    """获取可选端口"""
+    ports = list(comports(include_links=False))
+    available_ports_list = [port.device for port in ports]
+    return available_ports_list
+
+
+class FuboPneumaticFingerClient(PeripheralHandBase):
+    """富伯客户端"""
+
+    FLEX_CMD = b"F"
+    EXTEND_CMD = b"E"
+    BALL_CMD = b"B"
+    CYLINDER_CMD = b"C"
+    DOUBLE_CMD = b"D"
+    TREBLE_CMD = b"T"
+
+    def __init__(self, init_params=None):
+        self.baud_rate = 9600
+        self.data_bite = 8
+        self.timeout = 1
+        self.stop_bit = serial.STOPBITS_ONE  # 停止位
+        self.parity_bit = serial.PARITY_NONE  # 校验位
+        self.is_connected = False
+        self.ser = None  # 连接对象
+        self.port = "COM 4"
+        if init_params:
+            self.port = init_params["port"]
+
+    def __del__(self):
+        self.ser.close()
+
+    def connect(self):
+        try:
+            self.ser = serial.Serial(port=self.port,
+                                     baudrate=self.baud_rate,
+                                     parity=self.parity_bit,
+                                     stopbits=self.stop_bit,
+                                     timeout=self.timeout)
+            if self.ser.is_open:
+                self.is_connected = True
+                logger.info("connect")
+                return 1
+            else:
+                self.is_connected = False
+                logger.warning("open failed")
+                return 0
+        except OSError as e:
+            warning_info = f"pneumatic finger connect failed: {e}"
+            logger.warning(warning_info)
+            return 0
+
+    def flex(self):
+        self.ser.write(self.FLEX_CMD)
+        return self.ser.read()
+
+    def extend(self):
+        self.ser.write(self.EXTEND_CMD)
+        return self.ser.read()
+
+    def reconnect(self):
+        self.close()
+        return self.connect()
+
+    def init(self):
+        ret = self.connect()
+        return {"is_connected": self.is_connected, "msg": ret}
+
+    def start(self, train=None):
+        model = train.fubo_pneumatic_finger.pneumatic_finger_model
+        if model == "flex":
+            self.flex()
+        elif model == "ball":
+            self.ser.write(self.BALL_CMD)
+        elif model == "cylinder":
+            self.ser.write(self.CYLINDER_CMD)
+        elif model == "double":
+            self.ser.write(self.DOUBLE_CMD)
+        elif model == "treble":
+            self.ser.write(self.TREBLE_CMD)
+        time.sleep(7)
+        self.extend()
+        return 1
+
+    def stop(self):
+        return 1
+
+    def status(self):
+        status = {"is_connected": self.is_connected}
+        return status
+
+    def close(self):
+        if self.ser:
+            self.ser.close()
+            self.is_connected = False
+        return {"is_connected": self.is_connected}

+ 443 - 0
backend/core/peripheral/hand/ruishou.py

@@ -0,0 +1,443 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   ruishou.py
+@Time    :   2023/03/28 16:54:03
+'''
+
+import logging
+from socket import socket
+import struct
+import threading
+import time
+from typing import Optional
+
+from core.peripheral.hand.base import PeripheralHandBase
+from settings.config import settings
+
+hand_config = settings.config["hand_peripheral_parameter"]
+logger = logging.getLogger(__name__)
+
+
+def reconnect_decorator(func):
+    """重新连接装饰器"""
+
+    def inner(self, *args, **kwargs):
+        try:
+            return func(self, *args, **kwargs)
+        except Exception:
+            self.close()
+            self._start_client()
+            return func(self, *args, **kwargs)
+
+    return inner
+
+
+class Constants:
+    """睿手相关常量"""
+    CMD_LOCATION = 3
+    HANDSHAKE_CMD = 0x01
+    HEARTBEAT_CMD = 0x02
+    SET_CURRENT_CMD = 0x03
+    MOTION_CONTROL_CMD = 0x04
+    DRAFTING_ACTION_CMD = 0x05
+    FINISH_ACTION_CMD = 0x06
+
+    class SendPckLocation:
+        """睿手发送信号功能对于位置"""
+        HANDSHAKE_VERSION_H = 4
+        HANDSHAKE_VERSION_L = 5
+        SET_CURRENT_CMD = 4
+        SET_CURRENT_CHANNEL = 5
+        SET_CURRENT_VALUE = 6
+        MOTION_CONTROL_HAND = 4
+        MOTION_CONTROL_THUMB_BENDING = 5
+        MOTION_CONTROL_INDEX_FINGER_BENDING = 6
+        MOTION_CONTROL_MIDDLE_FINGER_BENDING = 7
+        MOTION_CONTROL_RING_FINGER_BENDING = 8
+        MOTION_CONTROL_LITTLE_FINGER_BENDING = 9
+        MOTION_CONTROL_DURATION = 10
+        DRAFTING_ACTION_HAND = 4
+        DRAFTING_ACTION_IS_ELECTRIC = 5
+        DRAFTING_ACTION_CHANNELS = 6
+        DRAFTING_ACTION_CHANNEL_A_VALUE = 7
+        DRAFTING_ACTION_CHANNEL_B_VALUE = 8
+        DRAFTING_ACTION_DURATION = 9
+
+    class RecvPckLocation:
+        """睿手接收信号功能对于位置"""
+        HANDSHAKE_STATUS = 4
+        HANDSHAKE_REASON = 5
+        SET_CURRENT_CHANNEL = 5
+        SET_CURRENT_VALUE = 6
+        MOTION_CONTROL_STATUS = 4
+        MOTION_CONTROL_DURATION = 5
+        DRAFTING_ACTION_STATUS = 4
+        DRAFTING_ACTION_DURATION = 5
+        FINISH_ACTION_STATUS = 4
+        FINISH_ACTION_DURATION = 5
+
+    class RecvStatus:
+        """睿手接收信号功能响应状态"""
+        HANDSHAKE_SUCCESS = 0x00
+        HANDSHAKE_FAIL = 0x01
+        HANDSHAKE_REASON_OTHER = 0x00
+        HANDSHAKE_REASON_DIFF_VERSION = 0x01
+
+    class PckValue:
+        """睿手数据包值"""
+        BOTH_HANDS = 0x01
+        LEFT_HAND = 0x02
+        RIGHT_HAND = 0x03
+
+
+class RuishouConnector:
+    """睿手客户端
+
+    功能:连接;发送心跳包;开启线程发送信号;接收信号等
+    """
+
+    def __init__(self) -> None:
+        self.__host = hand_config["hand_host"]
+        self.__port = hand_config["hand_port"]
+        self.__heart_interval = hand_config["hand_heart"]
+        self.__hand_version = hand_config["hand_version"]
+        self.__addr = (self.__host, self.__port)
+        self.protocol = Protocol()
+        self.sock = None
+        self.is_connected = False
+
+    def start_client(self):
+        """启动客户端"""
+        version_parm = {
+            Constants.SendPckLocation.HANDSHAKE_VERSION_H:
+                self.__hand_version[0],
+            Constants.SendPckLocation.HANDSHAKE_VERSION_L:
+                self.__hand_version[1]
+        }
+        sock = socket()
+        # 链接服务端地址
+        logger.info("Connecting to and hand peripheral...")
+        self.sock = sock
+        self.sock.connect(self.__addr)
+        logger.info("Hand Peripheral connected successfully.")
+        # TODO: 连接失败的logger
+        self.sync_send_data("handshake", version_parm)
+        self.is_connected = True
+        logger.info("handshake...")
+        # 向服务端发送心跳包
+        send_heartbeat_t = threading.Thread(target=self.__send_beat_data,
+                                            args=())
+        # recv_t.setDaemon(True)
+        # TODO: 目前启动线程接收会导致同步接口无法接受到数据(接收线程已接收), 后续可考虑上锁解决
+        # recv_t.start()
+        send_heartbeat_t.start()
+
+    def create_send_t(self, send_data):
+        """外部程序调用"""
+        send_t = threading.Thread(target=self.send_data, args=(send_data,))
+        send_t.start()
+        send_t.join()
+
+    def send_data(self, cmd, update=None):
+        if update is None:
+            update = dict()
+        send_data = self.protocol.get_pck(cmd, update)
+        self.sock.sendall(send_data)
+
+    def sync_send_data(self, cmd, update=None):
+        """同步接口"""
+        if update is None:
+            update = dict()
+        send_data = self.protocol.get_pck(cmd, update)
+        if send_data:
+            self.sock.send(send_data)
+            res = self.filter_recv_msg()
+            return res
+        else:
+            return None
+
+    def filter_recv_msg(self):
+        times = 0
+        res = {"msg": "fail"}
+        while times <= 50:
+            recv = self.sock.recv(1024)
+            recv_ls = self.protocol.unpack_bytes(recv)
+            for recv in recv_ls:
+                res = self.protocol.parse_bytes(recv)
+                if res["cmd"] != 2:
+                    return res
+            times += 1
+        return res
+
+    def __send_beat_data(self):
+        try:
+            while True:
+                self.is_connected = True
+                self.send_data("heartbeat")
+                time.sleep(self.__heart_interval)
+        except Exception:
+            logger.info(
+                "Send beat data failed, Hand Peripheral may be disconnected.")
+            self.is_connected = False
+
+    def close_client(self):
+        if self.sock:
+            self.sock.close()
+            self.is_connected = False
+
+
+class Protocol:
+    """睿手封装/解析包"""
+
+    def __init__(self) -> None:
+        self.pck_map = {
+            "handshake": [
+                0xAC, 0xAD, 0x05, 0x01, None, None, 0xFF, 0xFF, None, None
+            ],
+            "heartbeat": [
+                0xAC, 0xAD, 0x05, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, None, None
+            ],
+            "default_current": [
+                0xAC, 0xAD, 0x05, 0x03, None, None, None, 0xFF, None, None
+            ],
+            "motion_control": [
+                0xAC, 0xAD, 0x08, 0x04, None, None, None, None, None, None,
+                None, None, None
+            ],
+            "drafting_action": [
+                0xAC, 0xAD, 0x07, 0x05, None, None, None, None, None, None,
+                None, None
+            ],
+            "finish_action": [
+                0xAC, 0xAD, 0x05, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, None, None
+            ]
+        }
+        # 映射结果命令对应结果文本
+        self.recv_map = {
+            "handshake": {
+                "byte0": {
+                    0x00: "success",
+                    0x01: "fail"
+                },
+                "byte1": {
+                    0x00: "other",
+                    0x01: "diff version"
+                }
+            },
+            "heartbeat": {""}
+        }
+        self.recv_head = (0xAE, 0xAF)  # 睿手端数据包头
+
+    @staticmethod
+    def cal_checksum(data):
+
+        b_checksum = sum(data).to_bytes(2, byteorder="big")
+        byte_h, byte_l = struct.unpack("2B", b_checksum)
+
+        return byte_h, byte_l
+
+    def get_pck(self, cmd, update_pck=None) -> Optional[bytearray]:
+        """根据命令,参数更新包组装数据包
+
+        Args:
+            cmd: 命令 ['handshake', 'heartbeat', 'default_current',
+            'motion_control', 'drafting_action', 'finish_action']
+            update_pck: 更新数据:电流参数等
+
+        Returns:符合协议的bytes指令
+
+        """
+        if update_pck is None:
+            update_pck = dict()
+        arr = self.pck_map.get(cmd, None)
+        if not arr:
+            return None
+        for key in update_pck.keys():
+            if key > len(arr):
+                return None
+            arr[key] = update_pck[key]
+        if None in arr[:-2]:
+            return None
+        bs = bytearray(0)
+        byte_h, byte_l = self.cal_checksum(arr[3:-2])
+        arr[-2], arr[-1] = byte_h, byte_l
+        for i in arr:
+            bs += bytearray(i.to_bytes(1, byteorder="little"))
+        return bs
+
+    def unpack_bytes(self, recv_bytes) -> list:
+        """将socket.recv数据按包头进行拆包
+
+        Args:
+            recv_bytes: 接收的数据包
+
+        Returns: 分割后的命令数组
+
+        """
+        recv_list = struct.unpack_from(f"{len(recv_bytes)}B", recv_bytes)
+        res_list = []
+        for idx in range(len(recv_list) - 1):
+            # 寻找包头
+            if recv_list[idx] == self.recv_head[0] and (recv_list[idx + 1]
+                                                        == self.recv_head[1]):
+                head_idx = idx
+                data_len = recv_list[head_idx + 2] + 5
+                res_list.append(recv_list[head_idx:data_len + head_idx])
+        return res_list
+
+    def parse_bytes(self, recv_nums) -> dict:
+        """解析封装结果信息
+
+        Args:
+            recv_nums: 单个数据包数组
+
+        Returns:封装后的结果信息
+
+        """
+        res = dict()
+        res["cmd"] = recv_nums[Constants.CMD_LOCATION]
+        if recv_nums[Constants.CMD_LOCATION] == Constants.HANDSHAKE_CMD:
+            res["status"] = recv_nums[
+                Constants.RecvPckLocation.HANDSHAKE_STATUS]
+            res["reason"] = recv_nums[
+                Constants.RecvPckLocation.HANDSHAKE_REASON]
+
+        elif recv_nums[Constants.CMD_LOCATION] == Constants.SET_CURRENT_CMD:
+            res["current_channel"] = recv_nums[
+                Constants.RecvPckLocation.SET_CURRENT_CHANNEL]
+            res["current_val"] = recv_nums[
+                Constants.RecvPckLocation.SET_CURRENT_VALUE]
+
+        elif recv_nums[Constants.CMD_LOCATION] == Constants.MOTION_CONTROL_CMD:
+            res["motion_control_status"] = recv_nums[
+                Constants.RecvPckLocation.MOTION_CONTROL_STATUS]
+            res["motion_control_duration"] = recv_nums[
+                Constants.RecvPckLocation.MOTION_CONTROL_DURATION]
+
+        elif recv_nums[Constants.CMD_LOCATION] == Constants.DRAFTING_ACTION_CMD:
+            res["drafting_action_status"] = recv_nums[
+                Constants.RecvPckLocation.DRAFTING_ACTION_STATUS]
+            res["drafting_action_duration"] = recv_nums[
+                Constants.RecvPckLocation.DRAFTING_ACTION_DURATION]
+
+        elif recv_nums[Constants.CMD_LOCATION] == Constants.FINISH_ACTION_CMD:
+            res["drafting_action_status"] = recv_nums[
+                Constants.RecvPckLocation.FINISH_ACTION_STATUS]
+            res["drafting_action_duration"] = recv_nums[
+                Constants.RecvPckLocation.FINISH_ACTION_DURATION]
+
+        return res
+
+
+class RuishouClient(PeripheralHandBase):
+
+    def __init__(self) -> None:
+        self.connector = RuishouConnector()
+        self.version = (0x01, 0x00)  # 睿手版本
+        self.model = None
+
+    def _start_client(self, version=None):
+        """启动连接"""
+        if self.connector.is_connected:
+            return 1, "already connect"
+        if not version:
+            version = self.version
+        try:
+            self.connector.start_client()
+            return 1, "success connect"
+        except ConnectionRefusedError as e:
+            return 0, f"fail, {e}"
+
+    def _set_current(self, channel, val):
+        """调节预设电流"""
+        if not self.connector.is_connected:
+            return 0
+        parm_d = dict()
+        parm_d[Constants.SendPckLocation.SET_CURRENT_CHANNEL] = channel
+        parm_d[Constants.SendPckLocation.SET_CURRENT_VALUE] = val
+        parm_d[Constants.SendPckLocation.SET_CURRENT_CMD] = 1  # 开始
+        self.connector.sync_send_data(cmd="default_current", update=parm_d)
+        parm_d[Constants.SendPckLocation.SET_CURRENT_CMD] = 3  # 调节
+        self.connector.sync_send_data(cmd="default_current", update=parm_d)
+        parm_d[Constants.SendPckLocation.SET_CURRENT_CMD] = 2  # 结束
+        self.connector.sync_send_data(cmd="default_current", update=parm_d)
+        return 1
+
+    @staticmethod
+    def _change_hand_data(hand):
+        if hand == "双手":
+            return Constants.PckValue.BOTH_HANDS
+        if hand == "左手":
+            return Constants.PckValue.LEFT_HAND
+        if hand == "右手":
+            return Constants.PckValue.RIGHT_HAND
+
+    @reconnect_decorator
+    def _control_motion(self, grabbing_param):
+        logger.info("Launch peripheral...")
+        parm_d = {
+            Constants.SendPckLocation.MOTION_CONTROL_HAND:
+                self._change_hand_data(grabbing_param.hand_select),
+            Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING:
+                grabbing_param.thumb,
+            Constants.SendPckLocation.MOTION_CONTROL_INDEX_FINGER_BENDING:
+                grabbing_param.index_finger,
+            Constants.SendPckLocation.MOTION_CONTROL_MIDDLE_FINGER_BENDING:
+                grabbing_param.middle_finger,
+            Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING:
+                grabbing_param.ring_finger,
+            Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING:
+                grabbing_param.little_finger,
+            Constants.SendPckLocation.MOTION_CONTROL_DURATION:
+                grabbing_param.duration
+        }
+        res = self.connector.sync_send_data(cmd="motion_control", update=parm_d)
+        logger.info("Launch peripheral success")
+        return res
+
+    @reconnect_decorator
+    def _drafting_action(self, drafting_param):
+        if not self.connector.is_connected:
+            return 0
+        parm_d = {
+            Constants.SendPckLocation.DRAFTING_ACTION_HAND:
+                drafting_param.hand_select,
+            Constants.SendPckLocation.DRAFTING_ACTION_IS_ELECTRIC:
+                drafting_param.is_electric,
+            Constants.SendPckLocation.DRAFTING_ACTION_CHANNELS:
+                drafting_param.draft_channel,
+            Constants.SendPckLocation.DRAFTING_ACTION_CHANNEL_A_VALUE:
+                drafting_param.a_channel_value,
+            Constants.SendPckLocation.DRAFTING_ACTION_CHANNEL_B_VALUE:
+                drafting_param.b_channel_value,
+            Constants.SendPckLocation.DRAFTING_ACTION_DURATION:
+                drafting_param.duration
+        }
+        res = self.connector.sync_send_data(cmd="motion_control", update=parm_d)
+        return res
+
+    def init(self):
+        _, msg = self._start_client()
+        ret = {"is_connected": self.connector.is_connected, "msg": msg}
+        return ret
+
+    def set_db_model(self, db_model):
+        self.model = db_model
+
+    def start(self, train):
+        ret_msg = self._control_motion(train.hand_peripherals)
+        return ret_msg
+
+    def stop(self):
+        res = self.connector.sync_send_data(cmd="finish_action")
+        return res
+
+    def status(self):
+        status = {"is_connected": self.connector.is_connected}
+        return status
+
+    def close(self):
+        self.connector.close_client()
+        ret = {"is_connected": False}
+        return ret

+ 33 - 0
backend/core/peripheral/manager.py

@@ -0,0 +1,33 @@
+'''
+@Author  :   liujunshen
+@Ide     :   vscode
+@File    :   manager.py
+@Time    :   2023/03/29 10:32:02
+'''
+
+from core.peripheral.factory import PeripheralHandFactory
+from core.peripheral.hand.base import PeripheralHandBase
+
+
+class PeripheralHandManager(PeripheralHandBase):
+    """机械手主入口"""
+
+    def __init__(self, device_name, init_params) -> None:
+        peripheral_hand_factory = PeripheralHandFactory()
+        self.client = peripheral_hand_factory.create_client(
+            device_name, init_params)
+
+    def init(self):
+        return self.client.init()
+
+    def start(self, train=None):
+        return self.client.start(train)
+
+    def stop(self):
+        return self.client.stop()
+
+    def status(self):
+        return self.client.status()
+
+    def close(self):
+        return self.client.close()

+ 11 - 0
backend/core/sig_chain/device/connector_factory.py

@@ -0,0 +1,11 @@
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.device.faker import FakerConnector
+from core.sig_chain.device.neo import NeoConnector
+
+
+class ConnectorFactory():
+    def create_connector(self, device: Device):
+        if device == Device.FAKER:
+            return FakerConnector()
+        elif device == Device.NEO:
+            return NeoConnector()

+ 86 - 0
backend/core/sig_chain/device/connector_interface.py

@@ -0,0 +1,86 @@
+from abc import ABCMeta, abstractmethod
+from enum import Enum
+
+import numpy as np
+
+from core.sig_chain.sig_buffer import CircularBuffer
+from core.sig_chain.sig_buffer import ParserNewsetWithTime
+from core.sig_chain.sig_save import SigSave
+
+
+class Device(Enum):
+    FAKER = 0
+    PONY = 1
+    NEO = 2
+
+
+class DataMode(Enum):
+    WAVE = 1
+    IMPEDANCE = 2
+
+
+# 使用 @dataclass 会导致pyindtaller打包失败,不要使用
+class DataBlockInBuf:
+    def __init__(self, data: np.ndarray, timestamp: int):
+        self.data = data
+        self.timestamp = timestamp  # ms
+
+
+class Connector(metaclass=ABCMeta):
+
+    def __del__(self):
+        self.stop()
+
+    def set_saver(self):
+        SAVE_UNIT_SECONDS = 1
+        assert SAVE_UNIT_SECONDS * 1000 >= self.sample_params.delay_milliseconds, \
+            'Buffer size >= delay_milliseconds must be satisfied!'
+        self.buffer_save = CircularBuffer(
+            SAVE_UNIT_SECONDS, # edf 每次存储1s的数据
+            self.sample_params.data_count_per_channel /
+            self.sample_params.sample_rate, self.sample_params.channel_labels,
+            self.sample_params.channel_types, self.sample_params.sample_rate,
+            ParserNewsetWithTime())
+        self.saver = SigSave(self.sample_params.channel_labels,
+                             self.sample_params.sample_rate,
+                             self.sample_params.physical_max,
+                             self.sample_params.physical_min)
+
+    def _save_data_when_buffer_full(self, data_block):
+        if self.saver and self.buffer_save and self.saver.is_ready:
+            self.buffer_save.update(data_block)
+            ret = self.buffer_save.get_sig('array')
+            if ret['status'] == 'ok':
+                self.saver.save_raw_data(ret['data'], ret['timestamp'][0])
+
+    @abstractmethod
+    def load_config(self):
+        return
+
+    @abstractmethod
+    def get_ready(self):
+        return
+
+    @abstractmethod
+    def is_connected(self):
+        return
+
+    @abstractmethod
+    def setup_wave_mode(self):
+        return
+
+    @abstractmethod
+    def setup_impedance_mode(self):
+        return
+
+    @abstractmethod
+    def receive_wave(self):
+        return
+
+    @abstractmethod
+    def receive_impedance(self):
+        return
+
+    @abstractmethod
+    def stop(self):
+        return

+ 3 - 0
backend/core/sig_chain/device/fake_sig/faker-server-setup.ps1

@@ -0,0 +1,3 @@
+pyinstaller -F ./sig_fake_server.py
+mkdir ./dist/bdf_data
+cp ../../../../static/config/config.json ./dist/config.json

+ 180 - 0
backend/core/sig_chain/device/fake_sig/sig_fake_server.py

@@ -0,0 +1,180 @@
+"""fake signal generator server"""
+
+import copy
+import json
+import os
+import socket
+import socketserver
+import struct
+import threading
+import time
+
+from apscheduler.schedulers.background import BackgroundScheduler
+import numpy as np
+
+# ========== 项目内执行 =============
+from core.sig_chain.device.fake_sig.sig_generator import SignalGenerator
+from core.sig_chain.device.fake_sig.sig_reader import SigReader
+from settings.config import settings
+
+config = settings.config
+# =================================
+
+# ========== 打包时执行 =============
+# from sig_generator import SignalGenerator
+# from sig_reader import SigReader
+
+# def get_config():
+#     with open("config.json", "r", encoding="utf8") as f:
+#         config_data = json.load(f)
+#     return config_data
+
+# config = get_config()
+# =================================
+
+
+class FakeSignalServer:
+    """生成假数据"""
+
+    def __new__(cls, faker_eeg_config, signal_generator_config):
+        if not hasattr(cls, "_instance"):
+            cls._instance = super(FakeSignalServer, cls).__new__(cls)
+        return cls._instance
+
+    class FakeSignalSocketServer(socketserver.BaseRequestHandler):
+        """socket服务端"""
+
+        def handle(self):
+            self.scheduler = BackgroundScheduler()
+            self.super_class = FakeSignalServer._instance
+            conn = self.request
+            while True:
+                try:
+                    ret_bytes = conn.recv(1024)
+                except Exception:
+                    self.scheduler.remove_all_jobs()
+                    self.scheduler.shutdown()
+                ret_str = str(ret_bytes, encoding="utf-8")
+                if ret_str == "start":
+                    self.scheduler.add_job(
+                        self._send_sig,
+                        "interval",
+                        max_instances=1,
+                        seconds=self.super_class.send_frequency * 0.001)
+                    self.scheduler.start()
+                elif ret_str == "restart" and self.super_class.source != "faker":
+                    self.super_class.sig_reader.restart()
+                elif ret_str == "shutdown":
+                    print("关闭")
+                    self.scheduler.remove_all_jobs()
+                    if self.scheduler.get_jobs():
+                        self.scheduler.shutdown()
+                    self.request.close()
+                    self.server.shutdown()
+                    break
+                else:
+                    self.request.sendall(
+                        bytes("error command", encoding="utf-8"))
+
+        def _send_sig(self):
+            timestamp_b = self.super_class.get_bytes_timestamp()
+            sig_data_b = self.super_class.pack_data(
+                self.super_class.send_id).tobytes()
+            data = timestamp_b + sig_data_b
+            self.request.sendall(data)
+            self.super_class.send_id += 1
+
+    def __init__(self, faker_eeg_config, signal_generator_config) -> None:
+        self.faker_eeg_config = faker_eeg_config
+        self.signal_generator_config = signal_generator_config
+        self.host = self.faker_eeg_config["host"]
+        self.port = self.faker_eeg_config["port"]
+        self.sock = socket.socket()
+        self.send_id = 1
+        self.source = self.faker_eeg_config["source"]  # 数据来源,faker时用假数据
+        self.send_frequency = self.faker_eeg_config[
+            "delay_milliseconds"]  # 发送频率(ms)
+        if not os.path.exists(self.source):
+            print(self.source, os.getcwd(), "path not exist")
+            self.source = "faker"
+        if self.source == "faker":
+            self._init_sig_faker()
+        else:
+            self._init_sig_reader()
+
+    def _init_sig_faker(self):
+        self.fs = self.faker_eeg_config["sample_rate"]  # 采样率
+        self.channel_num = self.faker_eeg_config["channel_count"]  # 频道总数
+        self.signal_types = self.faker_eeg_config["sig_types"]  # 通道对应信号类型
+        self.signal_length = int(self.fs * self.send_frequency * 0.001)  # 数据长度
+        self.init_array = np.zeros((self.channel_num, self.signal_length),
+                                   dtype=np.float32)
+        self.noise = self.signal_generator_config["noise"]
+        self.signal_generator = SignalGenerator(
+            self.fs, self.signal_length, self.send_frequency,
+            self.signal_generator_config)  # 假波信号生成器
+
+    def _init_sig_reader(self):
+        self.sig_reader = SigReader(self.source, self.send_frequency)
+        self.channel_num = self.sig_reader.channel_num
+        self.fs = self.sig_reader.fs
+        self.signal_length = self.sig_reader.signal_length
+
+    @staticmethod
+    def get_bytes_timestamp():
+        timestamp = time.time()
+        timestamp_b = struct.pack("d", timestamp)
+        return timestamp_b
+
+    def wgn(self, sig, snr):
+        ps = np.sum(abs(sig)**2) / len(sig)
+        pn = ps / (10**((snr / 10)))
+        noise = np.random.randn(len(sig)) * np.sqrt(pn)
+        signal_add_noise = sig + noise
+        return signal_add_noise
+
+    def start_server(self):
+        print("faker server start")
+        sig_sever = socketserver.ThreadingTCPServer((self.host, self.port),
+                                                    self.FakeSignalSocketServer)
+        sig_sever.serve_forever()
+
+    def _pack_faker_data(self, send_id):
+        signal_array = copy.deepcopy(self.init_array)
+        for idx, signal_type in enumerate(self.signal_types):
+            signal_array[idx] = self.signal_generator.generator_sig(
+                signal_type, send_id)
+            if self.noise:
+                signal_array[idx] = self.wgn(signal_array[idx], 6)
+            signal_array[idx] += self.signal_generator_config[
+                "baseline_shift"] * self.signal_generator_config["wave_height"]
+        return signal_array
+
+    def _pack_reader_data(self):
+        return self.sig_reader.generator_sig()
+
+    def pack_data(self, send_id):
+        """封装信号"""
+        # TODO 根据type值获取对应的信号(模拟信号,存储信号)
+        if self.source == "faker":
+            signal_array = self._pack_faker_data(send_id)
+        else:
+            signal_array = self._pack_reader_data()
+        return signal_array
+
+    def get_channel_type_map(self):
+        channel_type_map = {}
+        for idx, signal_type in enumerate(self.signal_types):
+            channel_type_map[idx] = signal_type
+
+        return channel_type_map
+
+
+if __name__ == "__main__":
+    sig_server = FakeSignalServer(
+        config["faker_eeg_config"],
+        config["faker_eeg_config"]["signal_generator_config"])
+    sig_server.start_server()
+    # sig_server = FakeSignalServer()
+    # t = threading.Thread(target=sig_server.start_server)
+    # t.start()

+ 120 - 0
backend/core/sig_chain/device/fake_sig/sig_generator.py

@@ -0,0 +1,120 @@
+"""signal generator"""
+import numpy as np
+from scipy import signal
+
+
+class SignalGenerator:
+    """生成假数据"""
+
+    def __init__(self, fs, signal_length, send_frequency, sig_config) -> None:
+        self.fs = fs  # 采样率
+        self.signal_length = signal_length  # 信号长度
+        self.sig_config = sig_config
+        self.send_id = None  # 发送id
+        self.frequency = self.sig_config["frequency"]  # 频率
+        self.pack_times = send_frequency * 0.001  # 单个包时间长度
+        self.wave_height = self.sig_config["wave_height"]
+        self._init_generator()
+
+    def _init_generator(self):
+        """初始化生成器相关数据"""
+        self.sin_sig_generator = self._generator_sin_sig()  # 正弦波生成器
+        self.square_sig_generator = self._generator_square_sig()  # 方波生成器
+        self.saw_tooth_generator = self._generator_saw_tooth_sig()  # 锯齿波生成器
+        self.sin_sig_cache = None
+        self.square_sig_cache = None
+        self.saw_tooth_sig_cache = None
+        self.generator_sin_iter_times = 0  # 正弦生成器计数
+        self.generator_square_iter_times = 0  # 方波生成器计数
+        self.generator_saw_tooth_iter_times = 0  # 锯齿波生成器计数
+        self.saw_tooth_sig = self._gen_saw_tooth_sig(
+            peak_nums=self.sig_config["saw_tooth_peak_num"])
+        self.saw_tooth_index = 0  # 记录锯齿波位置
+
+    def generator_sig(self, sig_type: str, send_id: int):
+        """_summary_采用生成器生成信号;同一次发送使用缓存;有新发送id时更新缓存
+
+        Args:
+            sig_type (str): 待生成的波类型
+            send_id (int): 发送信号id
+
+        Returns:
+            _type_: nparray
+        """
+        sig = None
+        if not self.send_id or send_id != self.send_id:
+            self.square_sig_cache = self.square_sig_generator.__next__()
+            self.sin_sig_cache = self.sin_sig_generator.__next__()
+            self.saw_tooth_sig_cache = self.saw_tooth_generator.__next__()
+            self.send_id = send_id
+        if sig_type == "square":
+            sig = self.square_sig_cache
+        if sig_type == "sin":
+            sig = self.sin_sig_cache
+        if sig_type == "saw_tooth":
+            sig = self.saw_tooth_sig_cache
+        return sig
+
+    def _gen_saw_tooth_sig(self, peak_nums=30):
+        """生成一段递增锯齿波"""
+        period_nums = self.fs // self.frequency
+        saw_tooth_sig = np.empty((0))
+        for peak in range(1, peak_nums + 1):
+            climb_arr = np.linspace(0, peak, period_nums // 2, endpoint=False)
+            zero_arr = np.zeros((period_nums // 2))
+            saw_tooth_sig = np.concatenate((saw_tooth_sig, climb_arr, zero_arr))
+        return saw_tooth_sig
+
+    def _generator_square_sig(self):
+        while True:
+            times = np.linspace(self.generator_square_iter_times,
+                                (self.generator_square_iter_times + 1),
+                                self.signal_length,
+                                endpoint=False) * self.pack_times
+            self.generator_square_iter_times += 1
+            yield self.wave_height * signal.square(
+                2 * np.pi * self.frequency * times)
+
+    def _generator_sin_sig(self):
+        while True:
+            times = np.linspace(self.generator_sin_iter_times,
+                                (self.generator_sin_iter_times + 1),
+                                self.signal_length,
+                                endpoint=False) * self.pack_times
+            self.generator_sin_iter_times += 1
+            yield self.wave_height * np.sin(2 * np.pi * self.frequency * times,
+                                            dtype=np.float32)
+
+    def _generator_saw_tooth_sig(self):
+        while True:
+            start = self.saw_tooth_index
+            end = self.saw_tooth_index + self.signal_length
+            if end <= len(self.saw_tooth_sig):
+                self.saw_tooth_index += self.signal_length
+                yield self.wave_height * self.saw_tooth_sig[start:end]
+            else:
+                new_end = end - len(self.saw_tooth_sig)
+                sig = np.concatenate(
+                    (self.saw_tooth_sig[start:], self.saw_tooth_sig[:new_end]))
+                self.saw_tooth_index = new_end
+                yield self.wave_height * sig
+
+    def _generator_saw_tooth_sig_2(self):
+        while True:
+            times = np.linspace(self.generator_saw_tooth_iter_times,
+                                (self.generator_saw_tooth_iter_times + 1),
+                                self.signal_length,
+                                endpoint=False)
+            if self.generator_saw_tooth_iter_times == 30:
+                self.generator_saw_tooth_iter_times = 0
+            self.generator_saw_tooth_iter_times += 1
+            sig = self.wave_height * signal.sawtooth(
+                times) * self.generator_saw_tooth_iter_times
+            sig = np.maximum(sig, 0)
+            yield sig
+
+    def _reset_generator(self):
+        self.sin_sig_generator = self._generator_sin_sig()
+        self.square_sig_generator = self._generator_square_sig()
+        self.saw_tooth_generator = self._generator_saw_tooth_sig()
+

+ 42 - 0
backend/core/sig_chain/device/fake_sig/sig_reader.py

@@ -0,0 +1,42 @@
+import pyedflib
+import numpy as np
+
+
+class SigReader:
+
+    def __init__(self, path, send_frequency) -> None:
+        self.path = path
+        self.current_index = 0
+        self.load_bdf()
+        self.signal_length = int(self.fs * send_frequency * 0.001)
+
+    def restart(self):
+        self.current_index = 0
+
+    def load_bdf(self):
+        with pyedflib.EdfReader(self.path) as f:
+            self.labels = f.getSignalLabels()
+            self.channel_num = len(self.labels)
+            self.fs = f.getSampleFrequencies()[0]
+            self.size = f.getNSamples()[0]
+            self.data = np.zeros((self.channel_num, self.size),
+                                 dtype=np.float32)
+            for idx in range(self.channel_num):
+                self.data[idx] = f.readSignal(idx)
+
+    def generator_sig(self, signal_length=None):
+        if not signal_length:
+            signal_length = self.signal_length
+        end = self.current_index + signal_length
+        if end >= self.size:
+            new_end = end - self.size
+            ret = np.concatenate(
+                (self.data[:, self.current_index:], self.data[:, :new_end]),
+                axis=1)
+            self.current_index = new_end
+        else:
+            ret = self.data[:, self.current_index:end]
+            self.current_index = end
+        return ret
+
+

+ 163 - 0
backend/core/sig_chain/device/faker.py

@@ -0,0 +1,163 @@
+"""接收假数据
+
+Typical usage example:
+
+    connector = FakerConnector()
+    if connector.get_ready():
+        for _ in range(20):
+            connector.receive_wave()
+    connector.stop()
+"""
+import logging
+import numpy as np
+import socket
+
+from core.sig_chain.device.connector_interface import Connector
+from core.sig_chain.device.connector_interface import DataBlockInBuf
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.utils import Observable
+from core.sig_chain.utils import Singleton
+
+logger = logging.getLogger(__name__)
+
+
+class SampleParams:
+
+    def __init__(self, channel_count, sample_rate, delay_milliseconds):
+        self.channel_count = channel_count
+        self.channel_labels = [
+            'T6', 'P4', 'Pz', 'M2', 'F8', 'F4', 'Fp1', 'Cz', 'M1', 'F7', 'F3',
+            'C3', 'T3', 'A1', 'Oz', 'O1', 'O2', 'Fz', 'C4', 'T4', 'Fp2', 'A2',
+            'T5', 'P3'
+        ][:self.channel_count]
+        # montage 中定义的通道类型
+        self.channel_types = (['eeg'] * 24)[:self.channel_count]
+        self.sample_rate = sample_rate
+        self.delay_milliseconds = delay_milliseconds
+        self.point_size = 4
+        self.timestamp_size = 8
+        self.data_count_per_channel = int(self.delay_milliseconds *
+                                          self.sample_rate / 1000)
+        self.data_block_size = self.channel_count * self.data_count_per_channel
+        self.buffer_size = self.timestamp_size + self.data_block_size * self.point_size
+        self.physical_max = 20000
+        self.physical_min = -20000
+
+    def refresh(self):
+        self.data_count_per_channel = int(self.delay_milliseconds *
+                                          self.sample_rate / 1000)
+        self.data_block_size = self.channel_count * self.data_count_per_channel
+        self.buffer_size = self.timestamp_size + self.data_block_size * self.point_size
+
+
+class FakerConnector(Connector, Singleton, Observable):
+
+    def __init__(self) -> None:
+        Observable.__init__(self)
+        self.device = Device.FAKER
+        self._host = '127.0.0.1'
+        self._port = 21112
+        self._addr = (self._host, self._port)
+        self._sock = None
+        self._timestamp = 0
+
+        self.sample_params = SampleParams(24, 1000, 250)
+
+        self._is_connected = False
+
+        self.buffer_save = None
+        self.saver = None
+
+    def load_config(self, config_info):
+        if config_info.get('host'):
+            self._host = config_info['host']
+            logger.info('Set host to: %s', self._host)
+        if config_info.get('port'):
+            self._port = config_info['port']
+            logger.info('Set port to: %s', self._port)
+        if config_info.get('channel_count'):
+            self.sample_params.channel_count = config_info['channel_count']
+            logger.info('Set channel count to: %s',
+                        self.sample_params.channel_count)
+        if config_info.get('channel_labels'):
+            assert len( config_info['channel_labels']) == \
+                self.sample_params.channel_count, \
+                'Mismatch of channel labels and channel count'
+            self.sample_params.channel_labels = config_info['channel_labels']
+            logger.info('Set channel labels to: %s',
+                        self.sample_params.channel_labels)
+        if config_info.get('sample_rate'):
+            self.sample_params.sample_rate = config_info['sample_rate']
+            logger.info('Set sample rate to: %s',
+                        self.sample_params.sample_rate)
+        if config_info.get('delay_milliseconds'):
+            self.sample_params.delay_milliseconds = config_info[
+                'delay_milliseconds']
+            logger.info('Set delay milliseconds to: %s',
+                        self.sample_params.delay_milliseconds)
+        # NOTICE: 放在最后执行,以确保更改对buffer生效
+        self._addr = (self._host, self._port)
+        self.sample_params.refresh()
+
+    def is_connected(self):
+        return self._is_connected
+
+    def get_ready(self):
+        self._sock = socket.socket()
+        try:
+            self._sock.connect(self._addr)
+            self._is_connected = True
+            self._sock.sendall(bytes('start', encoding='utf-8'))
+        except ConnectionRefusedError:
+            return False
+        return True
+
+    def setup_wave_mode(self):
+        return True
+
+    def setup_impedance_mode(self):
+        return False
+
+    def receive_wave(self):
+        try:
+            packet = self._sock.recv(self.sample_params.buffer_size)
+            # timestamp = struct.unpack_from("d", packet[:2])
+            packet_parse = np.frombuffer(packet, dtype=np.float32)
+            data_block = packet_parse[2:].reshape(
+                self.sample_params.channel_count,
+                self.sample_params.data_count_per_channel)
+            self._add_a_data_block_to_buffer(data_block)
+            return True
+        except ConnectionAbortedError:
+            return False
+        except IOError:
+            return False
+
+    def receive_impedance(self):
+        raise NotImplementedError
+
+    def _add_a_data_block_to_buffer(self, data_block: np.ndarray):
+        self._timestamp += int(1000 *
+                               self.sample_params.data_count_per_channel /
+                               self.sample_params.sample_rate)
+        data_block_in_buffer = DataBlockInBuf(data_block, self._timestamp)
+        self._save_data_when_buffer_full(data_block_in_buffer)
+        self.notify_observers(data_block_in_buffer)
+
+        return data_block
+
+    def stop(self):
+        if self._sock:
+            self._sock.close()
+        self._is_connected = False
+        self._timestamp = 0
+
+        if self.saver and self.saver.is_ready:
+            self.saver.close_edf_file()
+
+    def notify_observers(self, data_block):
+        for obj in self._observers:
+            obj.update(data_block)
+
+    def restart_wave(self):
+        self._sock.sendall(bytes('restart', encoding='utf-8'))

+ 58 - 0
backend/core/sig_chain/device/montage_base_model.py

@@ -0,0 +1,58 @@
+"""提供标准头模及电极信息。"""
+from typing import List
+
+import mne
+
+class MontageBase():
+    """根据EEG的10-20标准创建montge"""
+
+    def __init__(self, chan_labels: List[str], chan_types: List[str], fs):
+        """按mne的格式初始化数据info和montage
+
+        Args:
+            chan_labels (List[str]): 导联标签
+            chan_types (List[str]): 可以是任意str,一般写为"eeg"即可,注意要对每个chan_labels都定义
+            fs (float): 采样率
+        """
+        self.info = mne.create_info(chan_labels, ch_types=chan_types, sfreq=fs)
+        self.info.set_montage("standard_1020")
+        self.montage = self.info.get_montage()
+
+
+    def print_montage_names(self):
+        """列出mne内建的montage列表"""
+        builtin_montages = mne.channels.get_builtin_montages(descriptions=True)
+        for montage_name, montage_description in builtin_montages:
+            print(f"{montage_name}: {montage_description}")
+
+
+    # def load_montage(self, montage_name: str):
+    #     """载入montage"""
+    #     montage = mne.channels.make_standard_montage(montage_name)
+    #     return montage
+
+
+    def plot_montage(self):
+        """依据输入的montage绘制脑地形图"""
+        self.montage.plot()
+
+
+    def get_chan_labels(self):
+        """列出montage所有电极的标签"""
+        return self.montage.ch_names
+
+
+    def get_chan_positions(self):
+        """列出montage所有电极的坐标"""
+        return self.montage.get_positions()
+
+
+    def find_label_by_chan(self, chan: int):
+        """输入导联号返回对应的导联标签"""
+        return self.montage.ch_names[chan]
+
+
+    def find_chan_by_label(self, label: str):
+        """输入导联标签返回对应的导联号"""
+        chan_names = self.montage.ch_names
+        return chan_names.index(label)

+ 171 - 0
backend/core/sig_chain/device/neo.py

@@ -0,0 +1,171 @@
+"""接收neo软件转发的数据
+
+Typical usage example:
+
+    connector = NeoConnector()
+    if connector.get_ready():
+        for _ in range(20):
+            connector.receive_wave()
+    connector.stop()
+"""
+import logging
+import socket
+import struct
+
+import numpy as np
+
+from core.sig_chain.device.connector_interface import Connector
+from core.sig_chain.device.connector_interface import DataBlockInBuf
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.utils import Observable
+from core.sig_chain.utils import Singleton
+
+
+logger = logging.getLogger(__name__)
+
+
+def bytes_to_float32(packet: bytes, bytes_of_packet, bytes_per_point=4):
+    assert bytes_of_packet % bytes_per_point == 0, \
+        'Bytes_of_packet % Bytes_per_point != 0'
+    data_block = []
+    for ii in range( bytes_of_packet // bytes_per_point):
+        point_in_bytes = packet[ii * bytes_per_point:(ii + 1) * bytes_per_point]
+        value = struct.unpack('f', point_in_bytes)[0]
+        data_block.append(value)
+    return data_block
+
+
+class SampleParams:
+
+    def __init__(self):
+        self.channel_count = 9
+        self.channel_labels = [
+            'C3', 'FC3', 'CP5', 'CP1', 'C4', 'FC4', 'CP2', 'CP6', 'Fp1'
+        ][:self.channel_count]
+        # montage 中定义的通道类型
+        self.channel_types = (['eeg'] * 8 +
+                              ['misc'])[:self.channel_count]
+        self.sample_rate = 1000  # TODO: fixed?
+        self.data_count_per_channel = int(40 * self.sample_rate / 1000)
+        self.point_size = 4
+        # channel: 8 + 1, 一个包传40个点, float: 4 字节; 9 * 40 * 4 = 1440
+        self.buffer_size = \
+            self.channel_count * self.data_count_per_channel * self.point_size
+        self.data_block_size = self.channel_count * self.data_count_per_channel
+        # 设备将数据量化的物理数值区间
+        self.physical_max = 200000
+        self.physical_min = -200000
+        self.delay_milliseconds = int(self.data_count_per_channel /
+                                      self.sample_rate * 1000)
+
+    def refresh(self):
+        self.data_count_per_channel = int(40 * self.sample_rate / 1000)
+        self.delay_milliseconds = int(self.data_count_per_channel /
+                                      self.sample_rate * 1000)
+        self.buffer_size = \
+            self.channel_count * self.data_count_per_channel * self.point_size
+        self.data_block_size = self.channel_count * self.data_count_per_channel
+
+
+class NeoConnector(Connector, Singleton, Observable):
+
+    def __init__(self) -> None:
+        Observable.__init__(self)
+        self.device = Device.NEO
+        self._host = '127.0.0.1'
+        self._port = 8712
+        self._addr = (self._host, self._port)
+        self._sock = None
+        self._timestamp = 0
+
+        self.sample_params = SampleParams()
+
+        self._is_connected = False
+
+        self.buffer_save = None
+        self.saver = None
+
+    def load_config(self, config_info):
+        if config_info.get('host'):
+            self._host = config_info['host']
+            logger.info('Set host to: %s', self._host)
+        if config_info.get('port'):
+            self._port = config_info['port']
+            logger.info('Set port to: %s', self._port)
+        if config_info.get('channel_count'):
+            self.sample_params.channel_count = config_info['channel_count']
+            logger.info('Set channel count to: %s',
+                        self.sample_params.channel_count)
+        if config_info.get('channel_labels'):
+            assert len( config_info['channel_labels']) == \
+                self.sample_params.channel_count, \
+                'Mismatch of channel labels and channel count'
+            self.sample_params.channel_labels = config_info['channel_labels']
+            logger.info('Set channel labels to: %s',
+                        self.sample_params.channel_labels)
+        if config_info.get('sample_rate'):
+            self.sample_params.sample_rate = config_info['sample_rate']
+            logger.info('Set sample rate to: %s',
+                        self.sample_params.sample_rate)
+        # NOTICE: 放在最后执行,以确保更改对相关参数生效
+        self.sample_params.refresh()
+        self._addr = (self._host, self._port)
+
+    def is_connected(self):
+        return self._is_connected
+
+    def get_ready(self):
+        self._sock = socket.socket()
+        try:
+            self._sock.connect(self._addr)
+            self._is_connected = True
+        except ConnectionRefusedError:
+            return False
+        return True
+
+    def setup_wave_mode(self):
+        return True
+
+    def setup_impedance_mode(self):
+        return False
+
+    def receive_wave(self):
+        try:
+            packet = self._sock.recv(self.sample_params.buffer_size)
+            data_block = np.frombuffer(packet, dtype=np.float32).reshape(
+                self.sample_params.data_count_per_channel,
+                self.sample_params.channel_count).T
+            self._add_a_data_block_to_buffer(data_block)
+            return True
+        except ConnectionAbortedError:
+            return False
+        except OSError:
+            return False
+        except ValueError:
+            return False
+
+    def receive_impedance(self):
+        raise NotImplementedError
+
+    def _add_a_data_block_to_buffer(self, data_block: np.ndarray):
+        self._timestamp += int(1000 *
+                               self.sample_params.data_count_per_channel /
+                               self.sample_params.sample_rate)
+        data_block_in_buffer = DataBlockInBuf(data_block,
+                                              self._timestamp)
+        self._save_data_when_buffer_full(data_block_in_buffer)
+        self.notify_observers(data_block_in_buffer)
+        # return data_block_2d
+
+    def stop(self):
+        if self._sock:
+            self._sock.close()
+        self._is_connected = False
+        self._timestamp = 0
+
+        if self.saver and self.saver.is_ready:
+            self.saver.close_edf_file()
+
+    def notify_observers(self, data_block):
+        for obj in self._observers:
+            obj.update(data_block)

+ 323 - 0
backend/core/sig_chain/pre_process.py

@@ -0,0 +1,323 @@
+"""对信号进行预处理,主要用于在线/离线算法、绘图之前"""
+from typing import List
+from typing import Optional
+
+import mne
+import numpy as np
+from scipy import signal
+
+
+class PreProcessor(object):
+    """信号预处理,包含去基漂,滤波,重参考以及重采样"""
+
+
+    @classmethod
+    def re_reference(cls,
+                     mne_raw_data,
+                     methods="average",
+                     ref_channels: Optional[List[str]] = None):
+        """对数据做重参考,主要提供三种常见重参考方法,共平均,按导联,双极导联
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+            methods (str, optional): "single"按导联,biopolar双极导联,默认为"average"共平均.
+            ref_channels (Optional[List[str]], optional): 默认为None,指定时为一个导联标签的列表
+
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+        if methods == "single":
+            return mne_raw_data.copy().set_eeg_reference(
+                ref_channels=ref_channels)
+        elif methods == "biopolar":
+            return mne.set_bipolar_reference(mne_raw_data,
+                                             anode=ref_channels[0],
+                                             cathode=ref_channels[1])
+        elif methods == "average":
+            return mne_raw_data.copy().set_eeg_reference(
+                ref_channels="average")
+
+
+    @classmethod
+    def detrend(cls, mne_raw_data):
+        """去基漂-去均值
+
+        注意:在处理模拟数据(如正弦信号)时,若不足一个周期,处理结果不符预期
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+
+        sig_mean = np.mean(mne_raw_data.get_data(), axis=1)
+        sig_detrended = mne_raw_data.get_data() - sig_mean.reshape(
+            sig_mean.shape[0], 1)
+        return mne.io.RawArray(sig_detrended, mne_raw_data.info)
+
+
+    @classmethod
+    def detrend_by_linear(cls, mne_raw_data):
+        """去基漂-去线性
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+
+        # axis=0 列方向处理
+        sig_detrended = signal.detrend(mne_raw_data.get_data(), axis=-1)
+        return mne.io.RawArray(sig_detrended, mne_raw_data.info)
+
+
+    @classmethod
+    def filter(cls,
+               mne_raw_data,
+               l_freq: Optional[int] = 0.1,
+               h_freq: Optional[int] = 40):
+        """滤波
+        l_freq<h_freq:band pass;
+        l_freq>h_freq:band stop;
+        l_freq is not None and h_freq is None: high pass;
+        l_freq is None and h_freq is not None: low pass
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+            l_freq (Optional[int], optional): low截至频率. Defaults to 0.1.
+            h_freq (Optional[int], optional): high截至频率. Defaults to 40.
+
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+        return mne_raw_data.copy().filter(l_freq=l_freq, h_freq=h_freq)
+
+
+    @classmethod
+    def resample(cls, mne_raw_data, new_freq):
+        """降采样,该函数为了避免混叠,降采样之前会做滤波
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+            new_freq (float): 新的采样率
+
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+        return mne_raw_data.copy().resample(sfreq=new_freq)
+
+
+    @classmethod
+    def resample_direct(cls, mne_raw_data, new_freq):
+        """降采样,直接间隔抽样
+
+        Args:
+            mne_raw_data (mne.io.array.array.RawArray): "mne格式的数据"
+            new_freq (float): 新的采样率
+
+        Returns:
+            class: mne.io.array.array.RawArray
+        """
+        sig = mne_raw_data.get_data()
+        step = mne_raw_data.info["sfreq"] / new_freq
+        assert len(sig[0]) % step == 0, \
+            f"Length if sig ({len(sig)}) can not divided by step({step})"
+        sig_resampled = sig[:, 0::int(step)]
+        return mne.io.RawArray(sig_resampled, mne_raw_data.info)
+
+
+class RealTimeFilter(object):
+    """ 实时滤波器
+
+    输入设计好的滤波器参数(_ce_a, _ce_b),实时滤波。
+    y(n) = b(0)*x(n)+b(1)*x(n-1)...-a(1)*y(n-1)-a(2)*y(n-2)...
+
+    Attribute:
+        _order_a: 分母阶数
+        _order_b: 分子阶数
+        _buffer_x: 历史输入
+        _buffer_y: 历史输出
+        _pos_x: 最新数据点的位置
+        _pos_y:
+        _ce_a: 分母系数
+        _ce_b: 分子系数
+    """
+
+    def __init__(self, ce_a: List[float], ce_b: List[float]):
+        self._order_a = len(ce_a)
+        self._order_b = len(ce_b)
+
+        self._ce_a = ce_a
+        self._ce_b = ce_b
+
+        # 环形,old<- ->new
+        self._buffer_x = [0.0] * self._order_b  # 存储x(n-N)...x(n-1)
+        self._buffer_y = [0.0] * self._order_a # 存储y(n-N)...y(n-1)
+
+        self._pos_x = 0  # x(n)的存放位置
+        self._pos_y = 0  # y(n)的存放位置
+
+    def filter(self, xn):
+        self._buffer_x[self._pos_x] = xn
+
+        weighted_sum_x = self.cal_weighted_sum_x()
+        weighted_sum_y = self.cal_weighted_sum_y()
+        yn = weighted_sum_x - weighted_sum_y
+        self._buffer_y[self._pos_y] = yn
+
+        self._pos_x += 1
+        if self._pos_x == self._order_b:
+            self._pos_x = 0
+
+        self._pos_y += 1
+        if self._pos_y == self._order_a:
+            self._pos_y = 0
+
+        return yn
+
+    def cal_weighted_sum_x(self):
+        # b(0)*x(n)+b(1)*x(n-1)...b(N-1)*x(n-N+1)
+        weighted_sum_x = 0
+        for ii in range(self._order_b):
+            pos_x = (self._pos_x - ii + self._order_b) % self._order_b
+            weighted_sum_x += self._ce_b[ii] * self._buffer_x[pos_x]
+        return weighted_sum_x
+
+    def cal_weighted_sum_y(self):
+        # a(1)*y(n-1)+a(2)*y(n-2)...+a(N-1)*y(n-N+1)
+        weighted_sum_y = 0
+        for ii in range(1, self._order_a):
+            pos_y = (self._pos_y - ii + self._order_a) % self._order_a
+            weighted_sum_y += self._ce_a[ii] * self._buffer_y[pos_y]
+        return weighted_sum_y
+
+    @classmethod
+    def init_eeg(cls, code, fs=1000):
+        """初始化eeg常用实时滤波器
+
+        Args:
+            code (int): 预处理类型. 0代表0.5Hz高通,1代表60Hz低通.
+            fs (float, optional): 采样率
+
+        Returns:
+            RealTimFilter: 实时滤波器实例
+        """
+        assert code in [0, 1, 2], "Invalid code for eeg RealTimeFilter init!"
+        if code == 0:
+            # butter 0.5Hz高通
+            # aa = [1, -1.982228929792529, 0.982385450614125]
+            # bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+            bb, aa = signal.butter(2, [2*0.5/fs], "hp")
+        elif code == 1:
+            # 60Hz低通
+            # aa = [1, -0.031426266043351]
+            # bb = [0.484286866978324, 0.484286866978324]
+            bb, aa = signal.butter(1, [2*60/fs])
+        elif code == 2:
+            # 40Hz低通
+            # aa = [1, -0.290526856731916]
+            # bb = [0.354736571634042, 0.354736571634042]
+            bb, aa = signal.butter(1, [2*40/fs])
+
+        return cls(aa, bb)
+
+
+class RealTimeFilterM(object):
+    """ 对多个通道同时进行实时滤波器
+
+    输入设计好的滤波器参数(_ce_a, _ce_b),对每个通道进行实时滤波:
+    y(n) = b(0)*x(n)+b(1)*x(n-1)...-a(1)*y(n-1)-a(2)*y(n-2)...
+
+    Attribute:
+        _order_a: 分母阶数
+        _order_b: 分子阶数
+        _channel: 信号的通道数
+        _buffer_x: 历史输入
+        _buffer_y: 历史输出
+        _pos_x: 最新数据点的位置
+        _pos_y:
+        _ce_a: 分母系数
+        _ce_b: 分子系数
+    """
+
+    def __init__(self, ce_a: List[float], ce_b: List[float], channel: int):
+        self._order_a = len(ce_a)
+        self._order_b = len(ce_b)
+        self._channel = channel
+
+        self._ce_a = ce_a
+        self._ce_b = ce_b
+
+        # 环形,old<- ->new
+        self._buffer_x = np.zeros((self._channel, self._order_b),
+                                  dtype=np.float64)  # 存储x(n-N)...x(n-1)
+        self._buffer_y = np.zeros((self._channel, self._order_a),
+                                  dtype=np.float64) # 存储y(n-N)...y(n-1)
+
+        self._pos_x = 0  # x(n)的存放位置
+        self._pos_y = 0  # y(n)的存放位置
+
+    def filter(self, xn: np.ndarray):
+        self._buffer_x[:, self._pos_x] = xn
+
+        weighted_sum_x = self.cal_weighted_sum_x()
+        weighted_sum_y = self.cal_weighted_sum_y()
+        yn = weighted_sum_x - weighted_sum_y
+        self._buffer_y[:, self._pos_y] = yn
+
+        self._pos_x += 1
+        if self._pos_x == self._order_b:
+            self._pos_x = 0
+
+        self._pos_y += 1
+        if self._pos_y == self._order_a:
+            self._pos_y = 0
+
+        return yn
+
+    def cal_weighted_sum_x(self):
+        # b(0)*x(n)+b(1)*x(n-1)...b(N-1)*x(n-N+1)
+        weighted_sum_x = np.zeros(self._channel, dtype=np.float64)
+        for ii in range(self._order_b):
+            pos_x = (self._pos_x - ii + self._order_b) % self._order_b
+            weighted_sum_x += self._ce_b[ii] * self._buffer_x[:, pos_x]
+        return weighted_sum_x
+
+    def cal_weighted_sum_y(self):
+        # a(1)*y(n-1)+a(2)*y(n-2)...+a(N-1)*y(n-N+1)
+        weighted_sum_y = np.zeros(self._channel, dtype=np.float64)
+        for ii in range(1, self._order_a):
+            pos_y = (self._pos_y - ii + self._order_a) % self._order_a
+            weighted_sum_y += self._ce_a[ii] * self._buffer_y[:, pos_y]
+        return weighted_sum_y
+
+    @classmethod
+    def init_eeg(cls, code, channel, fs=1000):
+        """初始化eeg常用实时滤波器
+
+        Args:
+            code (int): 预处理类型. 0代表0.5Hz高通,1代表60Hz低通.
+            channel(int): 通道数
+            fs (float, optional): 采样率. Defaults to 1000.
+
+        Returns:
+            RealTimFilterM: 实时滤波器实例
+        """
+        assert code in [0, 1, 2], "Invalid code for eeg RealTimeFilter init!"
+        if code == 0:
+            # butter 0.5Hz高通
+            # aa = [1, -1.982228929792529, 0.982385450614125]
+            # bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+            bb, aa = signal.butter(2, [2*0.5/fs], "hp")
+        elif code == 1:
+            # 60Hz低通
+            # aa = [1, -0.031426266043351]
+            # bb = [0.484286866978324, 0.484286866978324]
+            bb, aa = signal.butter(1, [2*60/fs])
+        elif code == 2:
+            # 40Hz低通
+            # aa = [1, -0.290526856731916]
+            # bb = [0.354736571634042, 0.354736571634042]
+            bb, aa = signal.butter(1, [2*40/fs])
+
+        return cls(aa, bb, channel)

+ 166 - 0
backend/core/sig_chain/sig_buffer.py

@@ -0,0 +1,166 @@
+"""环形buffer,用来缓存一定长度的数据"""
+import collections
+import itertools
+import math
+
+import mne
+import numpy as np
+
+from core.sig_chain.device.montage_base_model import MontageBase
+from core.sig_chain.utils import Observer
+
+
+class ParserNewset():
+    """策略方法类
+    """
+
+    def parser_newset(self,
+                      package_num,
+                      content,
+                      mbm_created,
+                      dataformat="mne"):
+        """策略类方法接口
+
+        Args:
+            package_num (int): 包的数量
+            content (dqueue): 数据队列
+            mbm_created (class): mne 的info
+            dataformat (str, optional): 数据类型,默认为mne格式,目前dataformat为任意其它
+            值都会返回nparray格式. Defaults to "mne".
+        """
+        pass
+
+
+class ParserNewsetWithTime(ParserNewset):
+    """类策略方法:解析有时间戳的数据
+
+    Args:
+        ParserNewset (class): 父类
+    """
+
+    def parser_newset(self,
+                      package_num,
+                      content,
+                      mbm_created,
+                      dataformat="mne"):
+        """解析数据有时间戳的数据
+
+        Args:
+            package_num (int): 包数量
+            content (class): dqueue
+            mbm_created (class): mne 的info
+            dataformat (str, optional): 数据类型,默认为mne格式,目前dataformat为任意其它
+            值都会返回nparray格式. Defaults to "mne".
+
+        Returns:
+            dict: 返回数据和状态和时间戳
+        """
+        status = "unknown"
+        if content and len(content) >= package_num:
+            data_list = []
+            time_list = []
+            for con in list(content):
+                data_list.append(con.data)
+                time_list.append(con.timestamp)
+
+            signals = np.concatenate(data_list, axis=1)
+            status = "ok"
+
+            raw_data = mne.io.RawArray(
+                signals, mbm_created.info) if dataformat == "mne" else signals
+            return {"status": status, "data": raw_data, "timestamp": time_list}
+        else:
+            return {"status": "warn", "data": None, "timestamp": None}
+
+
+class PaserNewsetWithoutTime(ParserNewset):
+    """类策略方法:解析没有时间戳的数据
+
+    Args:
+        ParserNewset (class): 父类
+    """
+
+    def parser_newset(self,
+                      package_num,
+                      content,
+                      mbm_created,
+                      dataformat="mne"):
+        """解析数据没有时间戳的数据
+
+        Args:
+            package_num (int): 包数量
+            content (class): dqueue
+            mbm_created (class): mne 的info
+            dataformat (str, optional): 数据类型,默认为mne格式,目前dataformat为任意其它
+            值都会返回nparray格式. Defaults to "mne".
+
+        Returns:
+            dict: 返回数据和状态
+        """
+        status = "unknown"
+        if content and len(content) >= package_num:
+            signals = np.concatenate(tuple(
+                list(itertools.islice(content, 0, None))),
+                                     axis=1)
+            status = "ok"
+
+            raw_data = mne.io.RawArray(
+                signals, mbm_created.info) if dataformat == "mne" else signals
+            return {"status": status, "data": raw_data}
+        else:
+            return {"status": "warn", "data": None}
+
+
+class CircularBuffer(Observer):
+    """环形buffer类"""
+
+    def __init__(self, data_len, package_len, chan_labels, chan_types, fs,
+                 parser):
+        """初始化一个环形buffer
+
+        Args:
+            data_len (float): 数据长度,以秒为单位,例如要缓存20s的数据,data_len值为20
+            package_len (float): 包长度,以秒为单位,例如设备每100ms发送一个包,则package_len值为0.1
+            chan_labels (List[str]): 导联标签
+            chan_types (List[str]): 可以是任意str,一般写为"eeg"即可,注意要对每个chan_labels都定义
+            fs (float): 采样率
+            parser (class): 数据解析,来自于ParserNewset的类策略
+        """
+        self.data_len = data_len
+        self.package_len = package_len
+        self.package_num = math.ceil(self.data_len / self.package_len)
+        self.chan_labels = chan_labels
+        self.fs = fs
+        self.content = collections.deque(maxlen=self.package_num)
+        self.mbm_created = MontageBase(chan_labels, chan_types, fs)
+        self._shape_status = {"ok": "ok", "warn": "warn"}
+        self.parser = parser
+
+    def update(self, newset):
+        """更新buffer中的数据
+
+        Args:
+            newset (np array or other): 设备定时发来的数据,一般为chan_count*samples的二维矩阵
+        """
+        # if newset.any():
+        # if newset:
+        self.content.append(newset)
+        # else:
+        #     pass
+
+    def get_sig(self, dataformat="mne", clear=True):
+        """获得数据并转为mne格式
+
+        Args:
+            dataformat (str): 数据类型,默认为mne格式,目前dataformat为任意其它值都会返回nparray格式
+            clear (bool): 是否清空buffer的标志,默认为清空
+
+        Returns:
+            dict: 一个字典,"status"表示得到的数据维度是否正确,"ok"表示正确,"warn"表示维度和预期不相符;
+                  "data"默认为mne格式,也可以为nparray,根据策略方法不同,需要时也会有时间戳的输出
+        """
+        ret = self.parser.parser_newset(self.package_num, self.content,
+                                        self.mbm_created, dataformat)
+        if ret["status"] == "ok" and clear:
+            self.content.clear()
+        return ret

+ 43 - 0
backend/core/sig_chain/sig_reader.py

@@ -0,0 +1,43 @@
+"""读取数据文件
+"""
+from typing import List
+
+import mne
+import numpy as np
+
+
+class Reader:
+    """读取bdf文件
+    """
+    def __init__(self) -> None:
+        self._montage = mne.channels.make_standard_montage('standard_1020')
+
+    def read(self, filename: str, ch_names: List[str]):
+        raw = mne.io.read_raw_bdf(filename, preload=True)
+        raw.set_montage(self._montage)
+        raw.pick_channels(ch_names=ch_names)
+
+        return raw
+
+    def fix_annotation(self, raw:mne.io.Raw):
+        """在线数据按秒打标签,这里将相同秒标签合并
+
+        Args:
+            raw (mne.io.Raw): eeg data
+        """
+        annotations = raw.annotations
+        for item in ['miFailed', 'miSuccess']:
+            if item in set(annotations.description):
+                annotations.rename({item: 'mi'})
+        all_idxes = np.arange(0, len(annotations))
+        valid_idxes = []
+        last_label = None
+        for ii, annot in enumerate(annotations):
+            if last_label != annot['description']:
+                last_label = annot['description']
+                valid_idxes.append(ii)
+        valid_idxes = np.array(valid_idxes)
+        delete_mask = ~np.isin(all_idxes, valid_idxes)
+        delete_idxes = all_idxes[delete_mask]
+        annotations.delete(delete_idxes)
+        raw.set_annotations(annotations)

+ 154 - 0
backend/core/sig_chain/sig_receive.py

@@ -0,0 +1,154 @@
+"""连接多种脑电设备,并对接收的数据进行预处理
+
+Typical usage example:
+
+    receiver = Receiver()
+    receiver.select_connector(Device.PONY)
+    if receiver.setup_connector():
+        receiver.start_receive_wave()
+
+    data_from_buffer = receiver.get_data_from_buffer('plot')
+    receiver.stop_receive()
+"""
+import threading
+import time
+
+from core.sig_chain.device import connector_factory as cf
+from core.sig_chain.device.connector_interface import DataMode
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.sig_buffer import ParserNewsetWithTime
+from core.sig_chain.sig_buffer import CircularBuffer
+from core.sig_chain.utils import Singleton
+
+
+class Receiver(Singleton):
+
+    def __init__(self) -> None:
+        if Receiver._init_flag:
+            return
+        Receiver._init_flag = True
+        self.connector_factory = cf.ConnectorFactory()
+        self.connector = None
+        self.is_ready = False
+        self.trial_num = 0  # TODO: 是否保留
+
+        self.buffer_plot = None
+        self.buffer_classify_online = None
+        self.lock = threading.Lock()
+
+    def select_connector(self,
+                         device: Device,
+                         buffer_plot_size_seconds: float,
+                         config_info: dict = None):
+        self.connector = self.connector_factory.create_connector(device)
+        if config_info:
+            self.connector.load_config(config_info)
+        # NOTICE: 放在load_config最后执行,以确保更改对buffer等生效
+        self.setup_buffers(buffer_plot_size_seconds)
+
+    def setup_buffers(self, buffer_plot_size_seconds):
+        BUFFER_CLASSIFY_ONLINE_SIZE_SECONDS = 1
+        # pylint: disable=line-too-long
+        assert buffer_plot_size_seconds * 1000 >= self.connector.sample_params.delay_milliseconds, \
+            'Buffer size >= delay_milliseconds must be satisfied!'
+        assert BUFFER_CLASSIFY_ONLINE_SIZE_SECONDS * 1000 >= self.connector.sample_params.delay_milliseconds, \
+            'Buffer size >= delay_milliseconds must be satisfied!'
+        # pylint: enable=line-too-long
+        parser = ParserNewsetWithTime()
+        self.buffer_plot = CircularBuffer(
+            buffer_plot_size_seconds,
+            self.connector.sample_params.data_count_per_channel /
+            self.connector.sample_params.sample_rate,
+            self.connector.sample_params.channel_labels,
+            self.connector.sample_params.channel_types,
+            self.connector.sample_params.sample_rate, parser)
+        self.buffer_classify_online = CircularBuffer(
+            BUFFER_CLASSIFY_ONLINE_SIZE_SECONDS,
+            self.connector.sample_params.data_count_per_channel /
+            self.connector.sample_params.sample_rate,
+            self.connector.sample_params.channel_labels,
+            self.connector.sample_params.channel_types,
+            self.connector.sample_params.sample_rate, parser)
+        self.connector.add_observer(self.buffer_plot)
+        self.connector.add_observer(self.buffer_classify_online)
+
+    def setup_connector(self):
+        assert self.connector is not None, 'Select a connector first!'
+        self.clear_all_buffer()
+        self.is_ready = self.connector.get_ready()
+        return self.is_ready
+
+    def clear_all_buffer(self):
+        if self.buffer_plot:
+            self.buffer_plot.content.clear()
+        if self.buffer_classify_online:
+            self.buffer_classify_online.content.clear()
+
+    def setup_receive_mode(self, mode: DataMode):
+        success = False
+        if mode == DataMode.WAVE:
+            self.clear_all_buffer()
+            success = self.connector.setup_wave_mode()
+        else:
+            success = self.connector.setup_impedance_mode()
+        self.is_ready = success
+        return success
+
+    def start_receive_wave(self):
+        assert self.is_ready, 'Receiver is not ready!'
+        task = threading.Thread(target=self.receive_wave, args=(True,))
+        task.start()
+
+    def receive_wave(self, need_lock=False):
+        """
+
+        Args:
+            need_lock:是否需要加锁,用于pony,因为直接调用这个函数是不需要加锁的;
+                而这个函数在另一个线程中执行时是需要加锁的
+
+        Returns:
+
+        """
+        while self.is_ready:
+            time.sleep(0.01)
+            if need_lock:
+                self.lock.acquire()
+            self.connector.receive_wave()
+            if need_lock:
+                self.lock.release()
+
+    def receive_impedance(self):
+        assert self.is_ready, 'Receiver is not ready!'
+        return self.connector.receive_impedance()
+
+    def stop_receive(self, need_lock=False):
+        """
+
+        Args:
+            need_lock:是否需要加锁,用于pony,因为如果不使用多线程接收数据,
+                那么停止设备时就不需要加锁
+
+        Returns:
+
+        """
+        if self.is_ready:
+            self.is_ready = False
+            if need_lock:
+                self.lock.acquire()
+            self.connector.stop()
+            if need_lock:
+                self.lock.release()
+
+    def get_data_from_buffer(self, buffer_type: str, data_format='mne'):
+        if not self.is_ready:
+            raise RuntimeError('Connecter has not been setup correctly !')
+        assert buffer_type in ['plot', 'resting_state', 'classify_online'], \
+            'Invalid buffer type'
+        if buffer_type == 'plot':
+            return self.buffer_plot.get_sig(data_format)
+        elif buffer_type == 'classify_online':
+            return self.buffer_classify_online.get_sig(data_format)
+
+    def reset_wave(self):
+        self.clear_all_buffer()
+        self.connector.restart_wave()

+ 132 - 0
backend/core/sig_chain/sig_save.py

@@ -0,0 +1,132 @@
+"""保存数据为bdf格式"""
+import logging
+
+import pyedflib
+
+logger = logging.getLogger(__name__)
+
+class SigSaveHigh():
+    """数据保存类:使用highlevel API,简化了保存代码,
+    一次保存一个文件"""
+
+    def __init__(self, mne_raw_data):
+        """初始化SigSave
+
+        Args:
+            mne_raw_data (class): mne.io.array.array.RawArray
+        """
+        self.raw_data = mne_raw_data
+
+
+    def save(self, file_name, name, gender):
+        """保存
+
+        Args:
+            file_name (str): 保存的文件名
+            name (Optional[str], optional): bdf头信息中的姓名. Defaults to None.
+            gender (Optional[str], optional): bdf头信息中的性别. Defaults to None.
+        """
+        signals = self.raw_data.get_data()
+        channel_names = self.raw_data.info.get_montage().ch_names
+        signal_headers = pyedflib.highlevel.make_signal_headers(
+            channel_names, sample_frequency=self.raw_data.info["sfreq"])
+        header = pyedflib.highlevel.make_header(patientname=name, gender=gender)
+        pyedflib.highlevel.write_edf(file_name, signals, signal_headers, header)
+
+
+class SigSave():
+    """数据保存类: 使用基本的保存API,可以持续将数据写入一个文件"""
+
+
+    def __init__(self, channel_labels, sample_rate, physical_max, physical_min):
+        """初始化保存数据类
+
+        Args:
+            channel_labels (list): 导联标签,例如:['C3','C4']
+            sample_rate (int): 采样率
+            physical_max (int): 最大物理值,与设备相关, 例如pony:375000 neo:200000
+            physical_min (int): 最小物理值,与设备相关, 例如pony:-375000 neo:-200000
+        """
+        self.channel_labels = channel_labels
+        self.sample_rate = sample_rate
+        self.physical_max = physical_max
+        self.physical_min = physical_min
+        self.is_ready = False
+        self.is_first = False
+
+
+    def set_edf_header(self, subject, filename, task_per_run, path):
+        """ 用于设置EDF头部信息
+
+        Args:
+            subject (class): 受试数据库实体
+            path (str): 存储数据路径
+        """
+        channel_info = []
+        channel_count = len(self.channel_labels)
+        self.path = path + "/" + filename
+        self.edf_w = pyedflib.EdfWriter(self.path,
+                                        channel_count,
+                                        file_type=pyedflib.FILETYPE_BDFPLUS)
+        for label_num in range(channel_count):
+            ch_dict = {
+                "label": self.channel_labels[label_num],
+                "dimension": "uV",
+                "sample_frequency": self.sample_rate,
+                "physical_max": self.physical_max,
+                "physical_min": self.physical_min,
+                "digital_max": 8388607,
+                "digital_min": -8388608,
+            }
+            channel_info.append(ch_dict)
+        self.edf_w.setSignalHeaders(channel_info)
+        self.edf_w.setPatientName(subject.name)
+        if subject.gender == "男":
+            self.edf_w.setGender(1)
+        elif subject.gender == "女":
+            self.edf_w.setGender(0)
+        self.edf_w.setBirthdate(subject.birthday)
+        self.edf_w.setPatientCode(subject.id_card)
+        self.edf_w.setRecordingAdditional(str(task_per_run))
+        self.is_ready = True
+        self.is_first = True
+        self.start_record_timestamp = 0
+
+
+    def save_raw_data(self, signals, timestamp=None):
+        """向打开的文件写入数据,可以持续写入,直到调用close_edf_file时则无法写入
+
+        Args:
+            signals (np array): 需要保存的数据,channels*samples
+        """
+        if self.is_ready:
+            if self.is_first and timestamp:
+                self.start_record_timestamp = timestamp
+                self.is_first = False
+            self.edf_w.writeSamples(signals)
+        else:
+            logger.info(
+                "not ready for save, maybe edf/bdf header has not been set")
+
+
+
+    def edf_data_mark(self, timestamp, mark: str):
+        """给数据打标记
+
+        Args:
+            timestamp (int): 标记的时间点
+            mark (str): 标记的信息
+        """
+        if self.is_ready:
+            time_seconds = (timestamp - self.start_record_timestamp) / 1000
+            self.edf_w.writeAnnotation(time_seconds, -1, mark)
+        else:
+            logger.info(
+                "not ready for save, maybe edf/bdf header has not been set")
+
+
+    def close_edf_file(self):
+        """关闭BDF文件
+        """
+        self.edf_w.close()
+        self.is_ready = False

+ 57 - 0
backend/core/sig_chain/utils.py

@@ -0,0 +1,57 @@
+from abc import ABCMeta
+from abc import abstractmethod
+import logging
+import threading
+
+logger = logging.getLogger(__name__)
+
+class Singleton(object):
+    _instance_lock = threading.Lock()
+    _init_flag = False
+
+    def __new__(cls, *args, **kw):
+        with Singleton._instance_lock:
+            if not hasattr(cls, '_instance'):
+                orig = super(Singleton, cls)
+                cls._instance = orig.__new__(cls, *args, **kw)
+        return cls._instance
+
+    @classmethod
+    def clear_instance(cls):
+        cls._init_flag = False
+        if hasattr(cls, '_instance'):
+            del cls._instance
+
+    def __init__(self) -> None:
+        if Singleton._init_flag:
+            return
+        Singleton._init_flag = True
+
+
+class Observable(object):
+
+    def __init__(self) -> None:
+        self._observers = []
+
+    def add_observer(self, observer):
+        if observer not in self._observers:
+            self._observers.append(observer)
+        else:
+            logger.error('Add observer %s failed !', observer)
+
+    def remove_observer(self, observer):
+        try:
+            self._observers.remove(observer)
+        except ValueError:
+            logger.error('Failed to remove: %s', observer)
+
+    @abstractmethod
+    def notify_observers(self):
+        return
+
+
+class Observer(metaclass=ABCMeta):  # Observer
+
+    @abstractmethod
+    def update(self):
+        pass

+ 281 - 0
backend/core/utils.py

@@ -0,0 +1,281 @@
+""" Common function for camera based method """
+from fractions import Fraction
+import json
+import logging
+import os
+import time
+
+import av
+import cv2
+import numpy as np
+
+from settings.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+class VideoAnalyser(object):
+    """摄像头/视频数据分析的基类, 实现逐帧分析
+
+    Attributes:
+        t_start_save_video (float): 开始保存视频的时间,当使用av保存视频时需要此参数计算pts
+        out_stream: 使用opencv保存数据时使用
+        container:使用av保存视频时使用
+        stream: 使用av保存时使用
+    """
+
+    def __init__(self, camera_id=0, input_video=None):
+        if not input_video:
+            # For webcam input:
+            self.camera_id = camera_id
+            self.cap = cv2.VideoCapture(camera_id)
+            # TODO: cv2.CAP_DSHOW 能加速摄像头开启,但会导致视频保存出错?
+            # self.cap = cv2.VideoCapture(
+            #     camera_id) if camera_id == 0 else cv2.VideoCapture(
+            #         camera_id, cv2.CAP_DSHOW)  # 调用外部摄像头需设置cv2.CAP_DSHOW
+            self.is_camera = True
+        else:
+            self.cap = cv2.VideoCapture(input_video)
+            self.is_camera = False
+
+        # self.cap.setExceptionMode(True)
+        # opencv 4.6 的自动旋转错误,采用自定义的旋转方式
+        # self.cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 0.0)
+        # self.rotate_code = self.check_rotation(
+        #     self.cap.get(cv2.CAP_PROP_ORIENTATION_META))
+        self.rotate_code = None
+        self.t_start_save_video = None
+
+        self.save_with_av = False
+        self.out_stream = None
+        self.container = None
+        self.stream = None
+        self.previous_pts = 0
+
+    def __del__(self):
+        # self.cap.release()
+        # logger.info('Camera(%s) closed.', self.__class__.__name__)
+        # if self.out_stream:
+        #     self.out_stream.release()
+        # if self.container and self.t_start_save_video:
+        #     self.release_container()
+        self.close()
+
+    def get_save_fps(self):
+        return int(self.cap.get(cv2.CAP_PROP_FPS))
+
+    def open_camera(self):
+        success = self.cap.open(self.camera_id)
+        if success:
+            logger.info('Open camera(%s) succeed.', self.__class__.__name__)
+        else:
+            logger.error('Open camera(%s) failed.', self.__class__.__name__)
+        # if camera_id == 0:
+        #     self.cap.open(camera_id)
+        # else:
+        #     self.cap.open(camera_id, cv2.CAP_DSHOW)
+
+    def close(self, only_save: bool = False):
+        """关闭摄像头与结束视频保存
+
+        如果only_save为true,则结束视频保存,但不关闭摄像头;否则关闭摄像头与结束视频保存
+
+        Args:
+            only_save (bool, optional): 是否仅结束视频保存. Defaults to False.
+        """
+        if not only_save:
+            self.cap.release()
+            logger.info('Camera(%s) closed.', self.__class__.__name__)
+        if self.out_stream:
+            self.out_stream.release()
+            self.out_stream = None
+        self.release_container()
+        self.container = None
+
+    def set_output_video(self, output_video, save_with_av=False):
+        """ 设置输出视频
+
+        使用摄像头的情况下,必须在开摄像头之后调用,否则参数获取失败,无法正确设置输出视频
+
+        Args:
+            output_video (string): 要保存的视频文件路径
+            save_with_av (bool, optional): 使用av库进行保存
+        """
+        self.save_with_av = save_with_av
+        if not self.save_with_av:
+            # video info
+            # fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
+            # NOTICE: 这里需用 avc1 否则前端无法正常显示
+            fourcc = cv2.VideoWriter_fourcc(*'avc1')
+            fps = self.get_save_fps()
+            frame_size = (int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
+                        int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
+
+            # file to save video
+            self.out_stream = cv2.VideoWriter(output_video, fourcc, fps,
+                                              frame_size)
+        else:
+            assert self.is_camera,\
+                'Do not save video with av when process recorded video!'
+            self.container = av.open(output_video, mode='w')
+            # NOTICE: 这里需使用 h264, 否则前端无法正常显示
+            self.stream = self.container.add_stream(
+                'h264', rate=int(self.cap.get(cv2.CAP_PROP_FPS)))  # alibi frame rate
+            self.stream.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+            self.stream.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+            self.stream.pix_fmt = 'yuv420p'
+            self.stream.codec_context.time_base = Fraction(
+                1, int(self.cap.get(cv2.CAP_PROP_FPS)))
+
+    def is_ok(self):
+        if self.cap and self.cap.isOpened():
+            return True
+        else:
+            logger.debug('Camera not ready!!!')
+            return False
+
+    def check_rotation(self, rotate):
+        rotate_code = None
+        if int(rotate) == 270:
+            rotate_code = cv2.ROTATE_90_CLOCKWISE
+        elif int(rotate) == 180:
+            rotate_code = cv2.ROTATE_180
+        elif int(rotate) == 90:
+            rotate_code = cv2.ROTATE_90_COUNTERCLOCKWISE
+
+        return rotate_code
+
+    def correct_rotation(self, frame, rotate_code):
+        return cv2.rotate(frame, rotate_code)
+
+    def process(self, save=True):
+        try:
+            success, image = self.cap.read()
+            if not success:
+                logger.debug('Ignoring empty camera frame.')
+
+            if self.rotate_code is not None:
+                image = self.correct_rotation(image, self.rotate_code)
+        except cv2.error as exc:
+            logger.error(
+                'read data from camera(%s) failed, it may be disconnected: %s',
+                self.__class__.__name__, exc)
+            raise exc
+        t_read = time.time()
+
+        if success and save:
+            self.save_video(image, t_read)
+
+        return success, image
+
+    def save_video(self, image, t_read):
+        if self.save_with_av:
+            self.save_video_with_av(image, t_read)
+        else:
+            self.save_video_with_opencv(image)
+
+    def save_video_with_opencv(self, image):
+        if not self.out_stream:
+            return
+        try:
+            assert self.out_stream.isOpened(), 'Cannot open video for writing'
+            self.out_stream.write(image)
+        except Exception as exc:
+            logger.error('Fail to save video %s: %s', self.out_stream, exc)
+
+    def save_video_with_av(self, image, t_start):
+        """Save video with [av](https://github.com/PyAV-Org/PyAV)
+
+        Args:
+            image (np.ndarray): frame to save
+            t_start (float): timestamp of this frame
+        """
+        if not self.container:
+            return
+        try:
+            if not self.t_start_save_video:
+                self.t_start_save_video = t_start
+
+            frame = av.VideoFrame.from_ndarray(image, format='bgr24')
+            # Presentation Time Stamp (seconds -> counts of time_base)
+            delta_t = t_start - self.t_start_save_video
+            if delta_t < 0.0:
+                return
+            pts = int(round(delta_t / self.stream.codec_context.time_base))
+            logger.debug('pts: %d', pts)
+            if pts > self.previous_pts:
+                frame.pts = pts
+                self.previous_pts = frame.pts
+                for packet in self.stream.encode(frame):
+                    self.container.mux(packet)
+        except ValueError as exc:
+            logger.debug('Fail to save frame of video %s: %s', self.container, exc)
+
+    def release_container(self):
+        if self.t_start_save_video:
+            self.av_finish_with_a_blank_frame()
+
+        # Close the file
+        if self.container:
+            self.container.close()
+        self.t_start_save_video = None
+        self.previous_pts = 0
+
+    def av_finish_with_a_blank_frame(self):
+        # finish it with a blank frame, so the "last" frame actually gets
+        # shown for some time this black frame will probably be shown for
+        # 1/fps time at least, that is the analysis of ffprobe
+        try:
+            image = np.zeros((self.stream.height, self.stream.width, 3),
+                            dtype=np.uint8)
+            frame = av.VideoFrame.from_ndarray(image, format='bgr24')
+            pts = int(
+                round((time.time() - self.t_start_save_video) /
+                    self.stream.codec_context.time_base))
+            logger.debug('last pts: %d', pts)
+            frame.pts = pts if pts > self.previous_pts else self.previous_pts + 1
+            for packet in self.stream.encode(frame):
+                self.container.mux(packet)
+
+            # Flush stream
+            for packet in self.stream.encode():
+                self.container.mux(packet)
+        except ValueError as exc:
+            logger.debug('Fail to save frame of video %s: %s', self.container, exc)
+
+    def generator(self):
+        while self.is_ok():
+            success, frame = self.process()
+            # 使用generator函数输出视频流, 每次请求输出的content类型是image/jpeg
+            if success:
+                # 因为opencv读取的图片并非jpeg格式,因此要用motion JPEG模式需要先将图片转码成jpg格式图片
+                ret, jpeg = cv2.imencode('.jpg', frame)
+                # t_end = time.time()
+                # logger.debug("Time for process: %fs", t_end - t_start)
+                yield (b'--frame\r\n'
+                       b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() +
+                       b'\r\n\r\n')
+
+
+def create_data_dir(subject_id, train_id):
+    """为保存视频数据创建文件夹
+
+    Args:
+        subject_id (_type_): _description_
+        train_id (_type_): _description_
+    """
+    path = f'{settings.DATA_PATH}/{subject_id}/{train_id}'
+    try:
+        os.makedirs(path)
+    except OSError:
+        logger.debug('Folder already exists!')
+    return path
+
+
+def json_generator(feeder):
+    while feeder.is_ok():
+        # time.sleep(1 / 30.0)
+        success, _, data = feeder.process(only_keypoint=False)
+        if success:
+            json_data = json.dumps(data)
+            yield f'data:{json_data}\n\n'

+ 3 - 0
backend/data/113981_train_2023-11-08_14h40.10.130.csv

@@ -0,0 +1,3 @@
+trials.thisRepN,trials.thisTrialN,trials.thisN,trials.thisIndex,thisRow.t,notes,exp_prepare.started,prepare.started,prepare.stopped,exp_prepare.stopped,before_mi.started,train_position.started,train_position.stopped,instruction.started,instruction.stopped,img_reststate.started,img_reststate.stopped,before_mi.stopped,mi_prepare.started,img_prepare.started,img_prepare.stopped,mi_prepare.stopped,mi_begin.started,img_right.started,img_right.stopped,mi_begin.stopped,mi_feedback.started,feedback.started,feedback.stopped,mi_feedback.stopped,mi_rest.started,img_rest.started,img_rest.stopped,mi_rest.stopped,end.started,mi_end.started,mi_end.stopped,end.stopped,participant,session,date,expName,psychopyVersion,frameRate,expStart,
+0,0,0,0,19.499602300000333,,0.0028822000003856374,0.011108100001365528,3.0168231000006926,3.000236900001255,3.000249700000495,3.0168231000006926,5.016809700000522,5.99997810000059,7.999778600000354,9.499738800001069,19.499602300000333,19.483591400001387,19.4868617000011,19.499602300000333,20.999705600001107,20.98328560000118,20.9833004000011,20.999705600001107,25.999785700001667,25.983295900001394,25.985413500000504,25.999785700001667,40.99930770000174,40.982900700000755,40.98291870000139,40.99930770000174,45.99908570000116,45.98301270000047,,,,,113981,001,2023-11-08_14h40.10.130,train,2023.2.3,60.01576013853301,2023-11-08 14h40.17.556160 +0800,
+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,45.98307460000069,45.99908570000116,50.999324100001104,50.982959900000424,,,,,,,,

BIN
backend/data/113981_train_2023-11-08_14h40.10.130.psydat


+ 3 - 0
backend/data/136851_train_2023-11-08_14h44.32.460.csv

@@ -0,0 +1,3 @@
+trials.thisRepN,trials.thisTrialN,trials.thisN,trials.thisIndex,thisRow.t,notes,exp_prepare.started,prepare.started,prepare.stopped,exp_prepare.stopped,before_mi.started,train_position.started,train_position.stopped,instruction.started,instruction.stopped,img_reststate.started,img_reststate.stopped,before_mi.stopped,mi_prepare.started,img_prepare.started,img_prepare.stopped,mi_prepare.stopped,mi_begin.started,img_right.started,img_right.stopped,mi_begin.stopped,mi_feedback.started,feedback.started,feedback.stopped,mi_feedback.stopped,mi_rest.started,img_rest.started,mi_rest.stopped,end.started,mi_end.started,end.stopped,participant,session,date,expName,psychopyVersion,frameRate,expStart,
+0,0,0,0,19.50875140000062,,0.0027036000010411954,0.010083700000905083,3.0088847000006353,2.9927213999999367,2.992735100000573,3.0088847000006353,5.010257100000672,6.010356300001149,8.009113899999647,9.497494000001097,19.50875140000062,19.49249080000118,19.492986299999757,19.50875140000062,21.008855699999913,20.993044000000737,20.99305699999968,21.008855699999913,26.009171499999866,25.994534100000237,25.9965916000001,26.009171499999866,41.008257200001026,40.992517300001055,40.992535999999745,41.008257200001026,45.99303239999972,,,,136851,001,2023-11-08_14h44.32.460,train,2023.2.3,60.237081103555326,2023-11-08 14h44.39.305711 +0800,
+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,45.99308759999985,46.01033930000085,50.992737399999896,,,,,,,,

BIN
backend/data/136851_train_2023-11-08_14h44.32.460.psydat


+ 3 - 0
backend/data/961814_train_2023-11-08_14h46.13.009.csv

@@ -0,0 +1,3 @@
+trials.thisRepN,trials.thisTrialN,trials.thisN,trials.thisIndex,thisRow.t,notes,exp_prepare.started,prepare.started,exp_prepare.stopped,before_mi.started,train_position.started,train_position.stopped,instruction.started,instruction.stopped,img_reststate.started,img_reststate.stopped,before_mi.stopped,mi_prepare.started,img_prepare.started,mi_prepare.stopped,mi_begin.started,img_right.started,mi_begin.stopped,mi_feedback.started,feedback.started,feedback.stopped,mi_feedback.stopped,mi_rest.started,img_rest.started,mi_rest.stopped,end.started,mi_end.started,end.stopped,participant,session,date,expName,psychopyVersion,frameRate,expStart,
+0,0,0,0,19.512596000000485,,0.0033862000000226544,0.012058399999659741,3.0091026999998576,3.0091154999990977,3.027404500000557,5.043840599999385,6.010429399999339,8.025372799998877,9.508921000000555,19.512596000000485,19.49231619999955,19.492899299999408,19.512596000000485,21.008653999999297,21.008666899999298,21.02643949999947,26.012353899999653,26.014006999999765,26.025905700000294,41.026435100000526,41.01247279999916,41.01249629999984,41.026435100000526,46.02042810000057,,,,961814,001,2023-11-08_14h46.13.009,train,2023.2.3,59.858231764065536,2023-11-08 14h46.19.754626 +0800,
+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,46.020476300000155,46.03043289999914,51.02541849999943,,,,,,,,

BIN
backend/data/961814_train_2023-11-08_14h46.13.009.psydat


+ 22 - 0
backend/db/models/subject.py

@@ -0,0 +1,22 @@
+"""subject model"""
+import streamlit as st
+
+
+def create_table(conn):
+    with conn.session as s:
+        s.execute('CREATE TABLE IF NOT EXISTS subject (name TEXT, gender TEXT, birthday DATE, create_time DATETIME);')
+        s.commit()
+
+
+def get_subjects(conn):
+    subjects = conn.query('select * from subject', ttl=0.05)
+    return subjects
+
+
+def create_subject(conn, subject_form):
+    with conn.session as s:
+        s.execute(
+            'INSERT INTO subject (name, gender, birthday, create_time) VALUES (:name, :gender, :birthday, :create_time);',
+            params=dict(name=subject_form['name'], gender=subject_form['gender'], birthday=subject_form['birthday'], create_time=subject_form['create_time'])
+        )
+        s.commit()

+ 22 - 0
backend/db/models/train.py

@@ -0,0 +1,22 @@
+"""train model"""
+import streamlit as st
+
+
+def create_table(conn):
+    with conn.session as s:
+        s.execute('CREATE TABLE IF NOT EXISTS train (position TEXT, trial_num INTEGER, start_time DATETIME, owner_name TEXT);')
+        s.commit()
+
+
+def get_trains(conn, sub_name):
+    trains = conn.query('select * from train where owner_name = :owner', ttl=0.05, params={'owner': sub_name})
+    return trains
+
+
+def create_train(conn, train_form):
+    with conn.session as s:
+        s.execute(
+            'INSERT INTO train (position, trial_num, start_time, owner_name) VALUES (:position, :trial_num, :start_time, :owner_name);',
+            params=dict(position=train_form['position'], trial_num=train_form['trial_num'], start_time=train_form['start_time'], owner_name=train_form['owner_name'])
+        )
+        s.commit()

+ 131 - 0
backend/logging.json

@@ -0,0 +1,131 @@
+{
+    "version": 1,
+    "disable_existing_loggers": false,
+    "formatters": {
+        "standard": {
+            "format": "%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s"
+        },
+        "api": {
+            "format": "%(asctime)s - %(levelname)s - %(message)s"
+        }
+    },
+    "filters": {},
+    "handlers": {
+        "default": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "INFO",
+            "formatter": "standard",
+            "filename": "./logs/info.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        },
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": "DEBUG",
+            "formatter": "standard"
+        },
+        "error_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "ERROR",
+            "formatter": "standard",
+            "filename": "./logs/errors.log",
+            "maxBytes": 10485760,
+            "backupCount": 20,
+            "encoding": "utf8"
+        },
+        "debug_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "DEBUG",
+            "filename": "./logs/debug.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "formatter": "standard",
+            "encoding": "utf8"
+        },
+        "api_file_handler": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "level": "INFO",
+            "filename": "./logs/api.log",
+            "maxBytes": 10485760,
+            "backupCount": 5,
+            "formatter": "api"
+        }
+    },
+    "loggers": {
+        "multipart.multipart":{
+            "level": "WARNING",
+            "propagate": false
+        },
+        "uvicorn.access": {
+            "handlers": [
+                "api_file_handler"
+            ]
+        },
+        "mne": {
+            "level": "ERROR"
+        },
+        "matplotlib": {
+            "level": "ERROR"
+        },
+        "PIL.PngImagePlugin": {
+            "level": "ERROR"
+        },
+        "core.gait_analysis": {
+            "handlers": [
+                "default",
+                "console"
+            ],
+            "level": "INFO",
+            "propagate": false
+        },
+        "core.facial_expression": {
+            "handlers": [
+                "default",
+                "error_file_handler",
+                "debug_file_handler",
+                "console"
+            ],
+            "level": "INFO",
+            "propagate": false
+        },
+        "core.posture": {
+            "handlers": [
+                "default",
+                "error_file_handler",
+                "debug_file_handler",
+                "console"
+            ],
+            "level": "INFO",
+            "propagate": false
+        },
+        "core.utils": {
+            "handlers": [
+                "default",
+                "error_file_handler",
+                "debug_file_handler",
+                "console"
+            ],
+            "level": "INFO",
+            "propagate": false
+        },
+        "core.sig_chain.device.pony": {
+            "handlers": [
+                "default",
+                "error_file_handler",
+                "debug_file_handler"
+            ],
+            "level": "INFO",
+            "propagate": false
+        }
+    },
+    "root":{
+        "handlers": [
+            "default",
+            "error_file_handler",
+            "debug_file_handler",
+            "console"
+        ],
+        "level": "DEBUG"
+    }
+}

+ 65 - 0
backend/main.py

@@ -0,0 +1,65 @@
+"""NEO entrypoint"""
+from datetime import datetime
+
+import streamlit as st
+
+from db.models import subject
+from components.remove_style import hide_footer
+
+
+def _set_main_page_config():
+    # set_page_config must be the first command,
+    # and must only be set once per page.
+    st.set_page_config(
+        page_title="NEO",
+        page_icon=":house:",
+    )
+    hide_footer()
+
+
+def _create_subject(conn):
+    with st.form("subject_form"):
+        st.write("创建用户")
+        name = st.text_input("姓名")
+        gender = st.radio("性别", ["男", "女"])
+        birthday = st.date_input("生日")
+        submitted = st.form_submit_button("确定")
+        if submitted:
+            create_time = datetime.strptime(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S")
+            sub_new = {"name": name, "gender": gender, "birthday": birthday, "create_time": create_time}
+            subject.create_subject(conn, sub_new)
+
+
+def _main_page_content():
+    st.write("# NEO! 👋")
+
+
+    conn = st.connection("sql_app", type="sql")
+    subject.create_table(conn)
+    _create_subject(conn)
+    subjects = subject.get_subjects(conn)
+    st.write("# 用户列表")
+
+    st.dataframe(subjects)
+
+    st.markdown(
+        """
+            ### 更多信息
+            - 点击查看 [Neuracle](http://www.neuracle.cn)
+        """
+    )
+
+    st.markdown(
+        """
+            版权所有 © 博睿康科技(常州)股份有限公司
+        """
+    )
+
+
+def start_app():
+    _set_main_page_config()
+    _main_page_content()
+    return True
+
+
+start_app()

+ 45 - 0
backend/pages/2_train.py

@@ -0,0 +1,45 @@
+"""train"""
+from datetime import datetime
+import os
+
+import streamlit as st
+
+from db.models import subject
+from db.models import train
+from components.remove_style import hide_footer
+
+
+def _create_train(conn, subjects):
+    with st.form("train_form"):
+        st.write("创建训练")
+        position = st.text_input("训练部位")
+        trial_num = st.number_input("训练次数", value=1, step=1)
+        owner_name = st.selectbox("用户", subjects.name.to_list())
+        submitted = st.form_submit_button("开始训练")
+        if submitted:
+            start_time = datetime.strptime(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "%Y-%m-%d %H:%M:%S")
+            train_new = {"position": position, "trial_num": int(trial_num), "start_time": start_time, "owner_name": owner_name}
+            train.create_train(conn, train_new)
+            os.system("python train_1.py")
+            return owner_name
+
+
+def render():
+    st.set_page_config(
+        page_title="train", page_icon=":chart_with_upwards_trend:"
+    )
+    hide_footer()
+
+    st.markdown("# Train")
+    st.sidebar.success("训练")
+    conn = st.connection("sql_app", type="sql")
+    train.create_table(conn)
+    subjects = subject.get_subjects(conn)
+    sub_name = _create_train(conn, subjects)
+    if sub_name:
+        trains = train.get_trains(conn, sub_name)
+        st.write("# 训练列表")
+        st.dataframe(trains)
+
+
+render()

+ 29 - 0
backend/pages/3_test.py

@@ -0,0 +1,29 @@
+"""IMU放置于人体模型后的motion capture分析,包括离线、在线。读入或在线接入数据进行波形绘制、数据分析及分析结果的人体模型渲染和结果图表"""
+import streamlit as st
+
+from components.remove_style import hide_footer
+
+
+def on_line():
+    st.button("在线")
+    st.sidebar.success("在线")
+
+
+def off_line():
+    st.button("离线")
+    st.sidebar.success("离线")
+
+
+def render():
+    st.set_page_config(page_title="test", page_icon=":running:")
+    hide_footer()
+    st.markdown("# Test")
+
+    on_off_switch = st.toggle("离线/在线")
+    if on_off_switch:
+        on_line()
+    else:
+        off_line()
+
+
+render()

+ 64 - 0
backend/schemas/hand_peripheral.py

@@ -0,0 +1,64 @@
+"""睿手相关参数模型"""
+from enum import Enum
+
+from pydantic import BaseModel
+from pydantic import Field
+
+
+class ChannelName(int, Enum):
+    CHANNEL_A = 0x01
+    CHANNEL_B = 0x02
+
+
+class DraftChannel(int, Enum):
+    SINGLE = 0x01
+    DOUBLE = 0x02
+
+
+class IsElectric(int, Enum):
+    WITH_ELECTRIC = 0x00
+    WITHOUT_ELECTRIC = 0x01
+
+
+class SetCurrent(BaseModel):
+    """set current pydantic model"""
+    channel: ChannelName = Field(
+        ...,
+        description=
+        "set peripheral hand current channel (channelA: 0x01, channelB: 0x02)")
+    value: int = Field(...,
+                       le=255,
+                       ge=0,
+                       description="set peripheral hand current value")
+
+
+class ControlMotion(BaseModel):
+    """control motion pydantic model"""
+    hand_select: str = Field()
+    thumb: int = Field(..., le=100, ge=0)
+    index_finger: int = Field(..., le=100, ge=0)
+    middle_finger: int = Field(..., le=100, ge=0)
+    ring_finger: int = Field(..., le=100, ge=0)
+    little_finger: int = Field(..., le=100, ge=0)
+    duration: int = Field(..., le=20, ge=5)
+
+
+class DraftingAction(BaseModel):
+    """drafting action pydantic model"""
+    hand_select: str = Field(
+        ...,
+        description="select control hand (double: 0x01, left: 0x02, right:0x03)"
+    )
+    is_electric: IsElectric = Field(
+        ..., description="model (with electric: 0x00, without electric: 0x01)")
+    draft_channel: DraftChannel = Field(
+        ..., description="select channel (a channel: 0x01, double: 0x02)")
+    a_channel_value: int = Field(...,
+                                 le=255,
+                                 ge=0,
+                                 description="set A channel hand current value")
+    b_channel_value: int = Field(...,
+                                 le=255,
+                                 ge=0,
+                                 description="set b channel hand current value")
+    duration: int = Field(..., le=20, ge=5)

+ 86 - 0
backend/schemas/subjects.py

@@ -0,0 +1,86 @@
+"""Module schemas/subjects verifies table data type"""
+from datetime import date
+from datetime import datetime
+from typing import List, Literal
+from typing import Optional
+from typing import Union
+
+from pydantic import BaseModel
+from pydantic import Field
+from pydantic import validator
+
+from schemas.trains import ShowTrain
+from settings.config import settings
+
+language = settings.config["lang"]
+message_dict = settings.get_message()
+message = message_dict[language]
+
+
+def get_timestamp() -> datetime:
+    return datetime.strptime(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+                             "%Y-%m-%d %H:%M:%S")
+
+
+class SubjectBase(BaseModel):
+    """Subject Base Pydantic Model"""
+    name: str
+    id_card: Union[str, None]
+    gender: Literal["男", "女"]
+    birthday: date
+    rehabilitation_parts: list
+    remarks: str = ""
+
+    @validator("birthday")
+    def validate_birthday_date(cls, value):
+        value = datetime.strptime(str(value), "%Y-%m-%d")
+        if value > datetime.now() or value < datetime.strptime(
+                "1880-01-01", "%Y-%m-%d"):
+            raise ValueError(message["form_error_gender"])
+        return value
+
+    @validator("rehabilitation_parts")
+    def validate_rehabilitation_parts_date(cls, value):
+        if len(value) == 0 or len(value) > 4:
+            raise ValueError(message["rehab_parts_length"])
+        for part in value:
+            if part not in ["左手", "右手", "左腿", "右腿"]:
+                raise ValueError(message["rehab_parts_value_error"])
+        return value
+
+
+class SubjectUpdate(SubjectBase):
+    pass
+
+
+class SubjectCreate(SubjectBase):
+    create_time: Optional[datetime] = Field(default_factory=get_timestamp)
+
+
+class ShowSubject(BaseModel):
+    """展示患者信息"""
+    id: str
+    name: str
+    id_card: str
+    age: int
+    gender: str
+    rehabilitation_parts: str
+    create_time: datetime
+
+    class Config():
+        orm_mode = True
+
+
+class ShowSubjectDetails(ShowSubject):
+    """展示患者详情"""
+    trains: List[ShowTrain]
+
+
+class TodayStats(BaseModel):
+
+    today_num_format: str
+
+
+class SubjectIds(BaseModel):
+
+    ids: List[str] = Field(...)

+ 66 - 0
backend/schemas/trains.py

@@ -0,0 +1,66 @@
+"""Module schemas/trains verifies table data type"""
+from datetime import datetime
+from typing import Literal, Optional
+from typing import Union
+
+from pydantic import BaseModel, Field
+
+from schemas.hand_peripheral import ControlMotion
+
+
+def get_timestamp() -> datetime:
+    return datetime.strptime(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+                             "%Y-%m-%d %H:%M:%S")
+
+
+class TrainBase(BaseModel):
+    position: Optional[str] = None
+    rank: Optional[str] = None
+    trial_num: Optional[str] = None
+
+
+class TrainUpdate(TrainBase):
+    # position: str
+    # rank: str
+    # trial_num: int
+    start_time: Optional[datetime] = Field(default_factory=get_timestamp)
+    end_time: Optional[datetime] = Field(default_factory=get_timestamp)
+
+
+class TrainCreate(TrainBase):
+    # position: str
+    # rank: str
+    # trial_num: int
+    start_time: Optional[datetime] = Field(default_factory=get_timestamp)
+    end_time: Optional[datetime] = Field(default_factory=get_timestamp)
+    device_param: Union[ControlMotion, None] = None
+    owner_id: str
+
+
+class ShowTrain(TrainBase):
+    position: str
+    rank: str
+    trial_num: int
+    start_time: datetime
+    end_time: datetime
+    grade: str = None
+    consume_time: int = None
+    accuracy: float = None
+    is_train: bool = False
+    medical_certificate: str = ""
+
+    class Config():
+        orm_mode = True
+
+class ShowTrainWithVideo(ShowTrain):
+    video_path: list
+
+
+class TrainResult(BaseModel):
+    grade: Literal["优秀", "良好", "尚可"] = Field()
+    accuracy: float = Field()
+    consume_time: float = Field()
+
+
+class TrainMedicalCertificate(BaseModel):
+    medical_certificate: str

+ 79 - 0
backend/settings/config.py

@@ -0,0 +1,79 @@
+"""Module core/configs provide project base settings"""
+import glob
+import json
+import logging
+import os
+import os.path
+
+
+class Settings:
+    PROJECT_NAME: str = 'Kraken'
+    DEVICE = 'neo'
+    CONFIG_INFO = {
+        'host': '127.0.0.1',
+        'port': 8712,
+        'channel_count': 9,
+        'sample_rate': 1000,
+        'delay_milliseconds': 40,
+        'buffer_plot_size_seconds': 0.04,
+        'channel_labels': [
+            'C3',
+            'FC3',
+            'CP5',
+            'CP1',
+            'C4',
+            'FC4',
+            'CP2',
+            'CP6',
+            'FP1'
+        ]
+    }
+    PROJECT_VERSION: str = '0.0.1'
+    DATA_PATH = './db/data'
+    TRAIN_PARAMS = {
+        'instruct_duration': 2 * 1000,
+        'rest_stim_duration': 60 * 1000,
+        'prepare_duration': 1.5 * 1000,
+        'mi_duration': 5 * 1000,
+        'rest_duration': 5 * 1000,
+        'sample_duration': 3 * 1000 # 训练样本长度
+    }  # milliseconds
+
+    def __init__(self):
+        self.config = {"lang": "zh"}
+
+    def get_message(self):
+        self.message = {}
+        msg_list = glob.glob('./static/config/message*.json')
+        for msg in msg_list:
+            filename = os.path.basename(msg)
+            msg_code, ext = os.path.splitext(filename)
+            with open(msg, 'r', encoding='utf8') as file:
+                self.message[msg_code.split('_')[1]] = json.load(file)
+        return self.message
+
+
+settings = Settings()
+
+
+def setup_logging(default_path='logging.json',
+                  default_level=logging.INFO,
+                  env_key='LOG_CFG'):
+    """Setup logging configuration
+
+    """
+    path = default_path
+    value = os.getenv(env_key, None)
+    if value:
+        path = value
+    if os.path.exists(path):
+        with open(path, 'rt', encoding='utf-8') as f:
+            config = json.load(f)
+        logging.config.dictConfig(config)
+    else:
+        logging.basicConfig(level=default_level)
+
+
+def set_deepface_env():
+    # os.environ['DEEPFACE_HOME'] = Path(__file__).resolve().parent.as_posix()
+    os.environ['DEEPFACE_HOME'] = settings.get_resource_dir(['']).as_posix()

+ 214 - 0
backend/static/config/config.json

@@ -0,0 +1,214 @@
+{
+    "lang": "zh",
+    "hospital": "XXX医院",
+    "URL": {
+        "base": "http://localhost:8000",
+        "ws_base": "ws://localhost:8000",
+        "static": "/static",
+        "camera_route": "/api/v1/motion/camera",
+        "camera_set_output": "/api/v1/motion/camera/set-output",
+        "close_camera_route": "/api/v1/motion/close-camera",
+        "eeg_data_read": "/api/v1/eeg/data",
+        "eeg_data_buffer": "/api/v1/eeg/data-buffer",
+        "eeg_device_connect": "/api/v1/eeg/eeg-device-connect",
+        "eeg_device_close": "/api/v1/eeg/eeg-model-close",
+        "eeg_restart_fake_data": "/api/v1/eeg/restart-fake-data",
+        "eeg_clf_reset": "/api/v1/eeg/eeg-clf-reset",
+        "eeg_pipeline_reset": "/api/v1/eeg/eeg-pipeline-reset",
+        "eeg_train_configs": "/api/v1/eeg/train-configs",
+        "initial_rest_state_run": "/api/v1/eeg/initial-rest-state-run",
+        "mi_state_run": "/api/v1/eeg/mi-state-run",
+        "rest_state_run": "/api/v1/eeg/rest-state-run",
+        "mi_test_run": "/api/v1/eeg/mi-test-run",
+        "eeg_edf_set_header": "/api/v1/eeg/eeg-edf-set-header",
+        "eeg_save_data": "/api/v1/eeg/eeg-save-data",
+        "eeg_result_data": "/api/v1/trains/{train_id}/result",
+        "api_train_medical_certificate": "/api/v1/trains/{train_id}/medical-certificate",
+        "impedance_model_connect": "/api/v1/eeg/impedance-model-connect",
+        "impedance_model_close": "/api/v1/eeg/impedance-model-close",
+        "impedance_data": "/api/v1/eeg/impedance-data",
+        "set_train_finish_flag": "/api/v1/eeg/set-train-finish-flag",
+        "get_train_finish_flag": "/api/v1/eeg/get-train-finish-flag",
+        "delete_train": "/api/v1/trains/{train_id}",
+        "raw_bdf_data_close": "/api/v1/eeg/eeg-edf-close",
+        "eeg_edf_mark": "/api/v1/eeg/eeg-edf-mark",
+        "get_today_stats": "/api/v1/subjects/today-stats",
+        "startup_peripheral": "/api/v1/trains/{train_id}/startup-peripheral",
+        "web_subjects": "/subjects",
+        "web_subjects_update": "/subjects/{subject_id}",
+        "web_subjects_details": "/subjects/{subject_id}/details",
+        "api_subjects_delete": "/api/v1/subjects/{subject_id}",
+        "web_trains_start": "/trains/{train_id}/start",
+        "web_trains_test": "/trains/{train_id}/test",
+        "web_trains_details": "/trains/{train_id}/details",
+        "api_subjects_autocomplete": "api/v1/subjects/autocomplete",
+        "api_peripheral_get_serial_ports": "/api/v1/peripheral/serial-ports",
+        "api_peripheral_hand_init": "/api/v1/peripheral/hand/init",
+        "api_peripheral_hand_start": "/api/v1/peripheral/hand/start",
+        "api_peripheral_hand_stop": "/api/v1/peripheral/hand/stop",
+        "api_peripheral_hand_status": "/api/v1/peripheral/hand/status",
+        "api_peripheral_hand_close": "/api/v1/peripheral/hand/close",
+        "api_mi_img_erds": "/api/v1/mi/img/erds",
+        "api_mi_img_csp": "/api/v1/mi/img/csp",
+        "api_mi_img_wpli": "/api/v1/mi/img/wpli",
+        "api_mi_img_psd": "/api/v1/mi/img/psd"
+    },
+    "resource": {
+        "camera_placeholder": "/images/camera_placeholder.png"
+    },
+    "camera": {
+        "id": 0,
+        "task": "record"
+    },
+    "test_parameter": {
+        "rest_decrease_time": 0,
+        "eeg_psd_class": 1,
+        "fake_data": false,
+        "verify": false,
+        "device": "neo"
+    },
+    "faker_eeg_config": {
+        "host": "127.0.0.1",
+        "port": 21112,
+        "channel_count": 24,
+        "sample_rate": 1000,
+        "delay_milliseconds": 100,
+        "buffer_plot_size_seconds": 0.1,
+        "channel_labels": [
+            "T6",
+            "P4",
+            "Pz",
+            "M2",
+            "F8",
+            "F4",
+            "Fp1",
+            "Cz",
+            "M1",
+            "F7",
+            "F3",
+            "C3",
+            "T3",
+            "A1",
+            "Oz",
+            "O1",
+            "O2",
+            "Fz",
+            "C4",
+            "T4",
+            "Fp2",
+            "A2",
+            "T5",
+            "P3"
+        ],
+        "sig_types": [
+            "saw_tooth",
+            "square",
+            "square",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin",
+            "sin"
+        ],
+        "source": "bdf_data/sample.bdf",
+        "signal_generator_config": {
+            "frequency": 2,
+            "wave_height": 100,
+            "saw_tooth_peak_num": 40,
+            "noise": true,
+            "baseline_shift": 0
+        }
+    },
+    "pony_eeg_config": {
+        "device_address": "192.168.1.88",
+        "triggerbox_address": "10.0.0.63",
+        "gain": 12,
+        "channel_count": 24,
+        "sample_rate": 1000,
+        "delay_milliseconds": 100,
+        "buffer_plot_size_seconds": 0.1,
+        "channel_labels": [
+            "T6",
+            "P4",
+            "Pz",
+            "M2",
+            "F8",
+            "F4",
+            "Fp1",
+            "Cz",
+            "M1",
+            "F7",
+            "F3",
+            "C3",
+            "T3",
+            "A1",
+            "Oz",
+            "O1",
+            "O2",
+            "Fz",
+            "C4",
+            "T4",
+            "Fp2",
+            "A2",
+            "T5",
+            "P3"
+        ]
+    },
+    "neo_eeg_config": {
+        "host": "127.0.0.1",
+        "port": 8712,
+        "channel_count": 9,
+        "sample_rate": 1000,
+        "delay_milliseconds": 40,
+        "buffer_plot_size_seconds": 0.04,
+        "channel_labels": [
+            "C3",
+            "FC3",
+            "CP5",
+            "CP1",
+            "C4",
+            "FC4",
+            "CP2",
+            "CP6",
+            "Fp1"
+        ]
+    },
+    "hand_peripheral_parameter": {
+        "hand_host": "192.168.1.1",
+        "hand_port": 21111,
+        "hand_version": [1, 0],
+        "hand_heart": 0.5
+    },
+    "frontend_plot":{
+        "sample_rate": 100,
+        "show_channel": [
+            "C3",
+            "FC3",
+            "CP5",
+            "CP1",
+            "C4",
+            "FC4",
+            "CP2",
+            "CP6",
+            "Fp1"
+        ],
+        "max_time": 10,
+        "update_duration": 50
+    }
+}

+ 38 - 0
backend/static/config/message_zh.json

@@ -0,0 +1,38 @@
+{
+    "update_success": "更新成功",
+    "delete_success": "删除成功",
+    "update_failed": "更新失败",
+    "delete_failed": "删除失败",
+    "create_success": "创建成功",
+    "create_failed": "创建失败",
+    "invalid_age_input": "无效年龄输入",
+    "invalid_gender_input": "无效性别输入",
+    "form_error_name": "请填写姓名",
+    "form_error_id_card": "请填写标识号码",
+    "form_error_gender": "请填写有效性别:男或女",
+    "form_error_age": "请填写有效年龄",
+    "form_error_birthday": "请填写出生年月",
+    "form_error_plan": "请填写康复计划",
+    "subject_id_missing": "用户记录没有找到",
+    "train_id_missing": "训练记录没有找到",
+    "open_camera_failed": "摄像头打开失败",
+    "close_camera_success": "摄像头关闭成功",
+    "name_require": "^请输入姓名",
+    "name_length": "^请输入姓名,中/英/符号,长度30以内",
+    "id_card_require": "^请输入病历号",
+    "id_exclusion": "^该病历号已存在,请重新输入",
+    "gender_require": "^请输入性别",
+    "rehab_parts_length": "^请选择至少一个康复部位",
+    "rehab_parts_value_error": "^请输入有效的康复部位",
+    "birth_require": "^请选择出生日期",
+    "birth_range": "^请确认您的年龄在5~100岁",
+    "ruishou_connect_failed": "睿手连接失败",
+    "ruishou_start_success": "睿手启动成功",
+    "ruishou_no_effect_part": "不是有效部位",
+    "hand_peripheral_not_init": "机械手未初始化",
+    "pneumatic_finger_init_success": "气动手初始化成功",
+    "pneumatic_finger_init_failed": "气动手初始化失败,请检查设备是否已启动并进入镜像模式",
+    "pneumatic_finger_operate_success": "气动手操作成功",
+    "pneumatic_finger_operate_failed": "气动手操作失败,请查看设备是否已启动并进入镜像模式",
+    "pneumatic_finger_close_success": "气动手关闭成功"
+}

+ 119 - 0
backend/tests/core/mi/test_csp.py

@@ -0,0 +1,119 @@
+""" CSP 单元测试 """
+# pylint: disable=missing-class-docstring
+import os
+
+import numpy as np
+
+from core.sig_chain.sig_reader import Reader
+from core.mi.eeg_csp import CspOffline
+from core.mi.eeg_csp import CSPBasedClassifier
+
+TEST_DATA_PATH = "tests/data/"
+PONY_BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, "eeg_raw_data.bdf")
+NEO_BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, "neo_eeg_raw_data.bdf")
+PONY_CSP_FILE_PATH = os.path.join(TEST_DATA_PATH, "pony_csp.png")
+NEO_CSP_FILE_PATH = os.path.join(TEST_DATA_PATH, "neo_csp.png")
+
+
+def setup_module():
+    if not os.path.exists(TEST_DATA_PATH):
+        os.makedirs(TEST_DATA_PATH)
+
+
+def teardown_module():
+    if os.path.exists(PONY_CSP_FILE_PATH):
+        os.remove(PONY_CSP_FILE_PATH)
+    if os.path.exists(NEO_CSP_FILE_PATH):
+        os.remove(NEO_CSP_FILE_PATH)
+
+
+class TestCSPBasedClassifier():
+
+    @classmethod
+    def setup_class(cls):
+        ch_names = [
+            "T6", "P4", "Pz", "M2", "F8", "F4", "Fp1", "Cz", "M1", "F7", "F3",
+            "C3", "T3", "A1", "Oz", "O1", "O2", "Fz", "C4", "T4", "Fp2", "A2",
+            "T5", "P3"
+        ]
+
+        reader = Reader()
+        cls.raw = reader.read(PONY_BDF_FILE_PATH, tuple(ch_names))
+        cls.raw.annotations.rename({
+            "trainSuccess": "mi",
+            "trainFailed": "mi",
+            "restState": "rest"
+        })
+        cls.raw.annotations.duration += 0.999
+
+    def setup_method(self):
+        self.clf = CSPBasedClassifier()
+
+    def test_train_and_test_with_same_sample_length(self):
+        ch_names = ["C3", "Cz", "C4"]
+        csp_offline = CspOffline()
+        epochs, _ = csp_offline.get_epochs(self.raw, tuple(ch_names))
+        labels = epochs.events[:, -1]
+
+        self.clf.fit(epochs.get_data(), labels)
+
+        predicts = self.clf.predict(epochs.get_data())
+        acc = np.sum(predicts == labels) / predicts.size
+
+        assert self.clf.is_trained
+        assert acc >= 0.8
+
+    def test_train_and_test_with_different_sample_length(self):
+        ch_names = ["C3", "Cz", "C4"]
+        csp_offline1 = CspOffline()
+        epochs1, _ = csp_offline1.get_epochs(self.raw, tuple(ch_names))
+
+        csp_offline3 = CspOffline()
+        csp_offline3.tmax = 3
+        epochs3, _ = csp_offline3.get_epochs(self.raw, tuple(ch_names))
+
+        labels = epochs1.events[:, -1]
+        self.clf.fit(epochs3.get_data(), labels)
+
+        predicts = self.clf.predict(epochs1.get_data())
+        acc = np.sum(predicts == labels) / predicts.size
+
+        assert self.clf.is_trained
+        assert acc >= 0.8
+
+
+def test_main_csp_offline_pony():
+    ch_names = [
+        "T6", "P4", "Pz", "F8", "F4", "Fp1", "Cz", "F7", "F3", "C3", "T3", "Oz",
+        "O1", "O2", "Fz", "C4", "T4", "Fp2", "T5", "P3"
+    ]
+
+    reader = Reader()
+    raw = reader.read(PONY_BDF_FILE_PATH, tuple(ch_names))
+    raw.annotations.rename({
+        "trainSuccess": "mi",
+        "trainFailed": "mi",
+        "restState": "rest"
+    })
+
+    csp_offline = CspOffline()
+    epochs, _ = csp_offline.get_epochs(raw, tuple(ch_names))
+    csp = csp_offline.process(epochs)
+    csp_offline.draw_image(csp, epochs.info, save_path=PONY_CSP_FILE_PATH)
+
+
+def test_main_csp_offline_neo():
+    ch_names = ["C3", "FC3", "CP5", "CP1", "C4", "FC4", "CP2", "CP6"]
+
+    reader = Reader()
+    raw = reader.read(NEO_BDF_FILE_PATH, tuple(ch_names))
+    raw.annotations.rename({
+        "trainSuccess": "mi",
+        "trainFailed": "mi",
+        "restState": "rest"
+    })
+
+    csp_offline = CspOffline()
+    epochs, _ = csp_offline.get_epochs(raw, tuple(ch_names))
+    csp = csp_offline.process(epochs)
+    csp_offline.draw_image(csp, epochs.info, save_path=NEO_CSP_FILE_PATH)

+ 46 - 0
backend/tests/core/mi/test_erds.py

@@ -0,0 +1,46 @@
+""" RED/ERS 单元测试 """
+import os
+
+from core.sig_chain.sig_reader import Reader
+from core.mi.eeg_erds import ErdErs
+
+TEST_DATA_PATH = "tests/data/"
+BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, "eeg_raw_data.bdf")
+ERDS_FILE_PATH = os.path.join(TEST_DATA_PATH, "erds.png")
+TFR_ERDS_FILE_PATH = os.path.join(TEST_DATA_PATH, "tfr_erds.png")
+
+
+def setup_module():
+    if not os.path.exists(TEST_DATA_PATH):
+        os.makedirs(TEST_DATA_PATH)
+
+
+def teardown_module():
+    if os.path.exists(ERDS_FILE_PATH):
+        os.remove(ERDS_FILE_PATH)
+    if os.path.exists(TFR_ERDS_FILE_PATH):
+        os.remove(TFR_ERDS_FILE_PATH)
+
+
+def test_main():
+    # ERD/ERS
+    # 左右手
+    # [ "C3", "C4" ]
+    ch_names = ["C3", "Cz", "C4"]
+
+    reader = Reader()
+    raw = reader.read(BDF_FILE_PATH, ch_names)
+    raw.annotations.rename({
+        "trainSuccess": "mi",
+        "trainFailed": "mi",
+        "restState": "rest"
+    })
+    raw.resample(200)
+
+    channels = ("C3", "C4")
+    erds = ErdErs(-1, 1)
+    epochs, event_id_pick = erds.get_epochs(raw, channels)
+    tfr = erds.process(epochs, (-1, 0), mode="percent")
+    erds.draw_image(tfr, channels, ERDS_FILE_PATH)
+    tfr = erds.process(epochs, (-1, 0))
+    erds.draw_tfr_image(tfr, event_id_pick, channels, TFR_ERDS_FILE_PATH)

+ 130 - 0
backend/tests/core/mi/test_psd.py

@@ -0,0 +1,130 @@
+""" core/mi/eeg_psd.py 单元测试 """
+# pylint: disable=missing-class-docstring
+import os
+import pytest
+
+import numpy as np
+
+from core.sig_chain.sig_reader import Reader
+from core.mi.eeg_psd import PSDBasedClassifier
+from core.mi.eeg_psd import Psd
+from tests.utils.core import get_epochs
+
+
+TEST_DATA_PATH = 'tests/data/'
+BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, 'eeg_raw_data.bdf')
+PONY_PSD_FILE_PATH = os.path.join(TEST_DATA_PATH, 'pony_psd.png')
+
+
+def setup_module():
+    if not os.path.exists(TEST_DATA_PATH):
+        os.makedirs(TEST_DATA_PATH)
+
+
+def teardown_module():
+    if os.path.exists(PONY_PSD_FILE_PATH):
+        os.remove(PONY_PSD_FILE_PATH)
+
+
+class TestPSDBasedClassifier():
+
+    @classmethod
+    def setup_class(cls):
+        ch_names = [
+            'T6', 'P4', 'Pz', 'M2', 'F8', 'F4', 'Fp1', 'Cz', 'M1', 'F7', 'F3',
+            'C3', 'T3', 'A1', 'Oz', 'O1', 'O2', 'Fz', 'C4', 'T4', 'Fp2', 'A2',
+            'T5', 'P3'
+        ]
+
+        reader = Reader()
+        cls.raw = reader.read(BDF_FILE_PATH, tuple(ch_names))
+        cls.raw.annotations.duration += 0.999
+
+    def setup_method(self):
+        self.clf = PSDBasedClassifier()
+
+    def generate_one_sample(self, channel_count, high_freq=10, low_freq=0.4):
+        # 生成信号:10Hz的正弦波 + 0.4Hz的正弦波
+        tt = np.linspace(0, 1, 1000, endpoint=False)
+        xx = np.sin(2 * np.pi * high_freq * tt) + np.sin(
+            2 * np.pi * low_freq * tt)
+        return np.stack([xx for ch in range(channel_count)])
+
+    def test_psd_feature_extract_return_correct_shape(self):
+        sample = self.generate_one_sample(1)
+        bp_sample = self.clf.psd_feature_extract(sample)
+        assert isinstance(bp_sample, float)
+        # assert (1,) == bp_sample.shape
+
+    def test_psd_feature_extract_get_higher_value_for_matched_signal(self):
+        sample_match = self.generate_one_sample(1)
+        sample_not_match = self.generate_one_sample(1, high_freq=30)
+        bp_match = self.clf.psd_feature_extract(sample_match)
+        bp_not_match = self.clf.psd_feature_extract(sample_not_match)
+        assert bp_match > bp_not_match
+
+    def test_fit_with_single_channel_data(self):
+        ch_names = ['C4']
+        epochs = get_epochs(self.raw, tuple(ch_names), 'restState', tmax=0.999)
+
+        train_success = self.clf.fit(epochs.get_data())
+        assert train_success
+
+    def test_fit_with_multi_channel_data(self):
+        ch_names = ['C3', 'C4']
+        epochs = get_epochs(self.raw, tuple(ch_names), 'restState', tmax=0.999)
+
+        train_success = self.clf.fit(epochs.get_data())
+        assert train_success
+
+    def test_predict_before_fit_note_allowed(self):
+        sample = self.generate_one_sample(1)
+        with pytest.raises(Exception):
+            self.clf.predict(sample[np.newaxis, :])
+
+    def test_predict_with_single_channel_data(self):
+        channel_count = 1
+        self.clf.is_trained = True
+
+        sample = self.generate_one_sample(channel_count)
+        pred = self.clf.predict(sample[np.newaxis, :])
+        assert pred in [0, 1]
+
+    def test_predict_with_multi_channel_data(self):
+        channel_count = 2
+        self.clf.is_trained = True
+
+        sample = self.generate_one_sample(channel_count)
+        pred = self.clf.predict(sample[np.newaxis, :])
+        assert pred in [0, 1]
+
+    def test_main(self):
+        ch_names = ['C4']
+        epochs = get_epochs(self.raw, tuple(ch_names), 'restState', tmax=0.999)
+        train_success = self.clf.fit(epochs.get_data())
+
+        predicts = self.clf.predict(epochs.get_data())
+        acc = np.sum(predicts == 0) / predicts.size
+
+        assert train_success
+        assert acc >= self.clf.acc_accepted
+
+        epochs_mi = get_epochs(self.raw,
+                               tuple(ch_names),
+                               'trainSuccess',
+                               tmax=0.999)
+        predicts = self.clf.predict(epochs_mi.get_data())
+        acc = np.sum(predicts == 1) / predicts.size
+        assert acc >= self.clf.acc_accepted
+
+def test_pony():
+    bdf_file_path = os.path.join(TEST_DATA_PATH, '5_3_right_hand.bdf')
+    ch_names = ['C3', 'C4']
+
+    reader = Reader()
+    raw = reader.read(bdf_file_path, tuple(ch_names))
+    reader.fix_annotation(raw)
+
+    psd = Psd(0.1, 40, 0, 3)
+    epochs = psd.get_epochs(raw, tuple(ch_names))
+    psd.draw_image(epochs, ch_names, PONY_PSD_FILE_PATH)

+ 40 - 0
backend/tests/core/mi/test_riemannian.py

@@ -0,0 +1,40 @@
+import numpy as np
+
+import mne
+from mne import create_info
+
+from core.mi.pipeline import BaselineModel
+
+
+class DataGenerator:
+    def __init__(self, fs, X, info):
+        self.fs = int(fs)
+        self.X = X
+        self.info = info
+
+    def get_data_batch(self, current_index):
+        # return 1s batch
+        # create mne object
+        data = self.X[:, current_index - self.fs:current_index].copy()
+        # append event channel
+        data = np.concatenate((data, np.zeros((1, data.shape[1]))), axis=0)
+        info = create_info([f'S{i}' for i in range(len(data))], self.info['sfreq'], ['ecog'] * (len(data) - 1) + ['misc'])
+        raw = mne.io.RawArray(data, info, verbose=False)
+        return {'data': raw}
+
+    def loop(self):
+        # 0.1s step
+        step = int(0.1 * self.fs)
+        for i in range(self.fs, self.X.shape[1] + 1, step):
+            yield i / self.fs, self.get_data_batch(i)
+
+def test_pipeline():
+    data = mne.io.read_raw("core/mi/raw_eeg.fif")
+    X = data.get_data()
+    info = data.info.copy()
+    gen = DataGenerator(info["sfreq"], X, info)
+    pipeline = BaselineModel("core/mi/bp-baseline.pkl")
+
+    for t, batch_data in gen.loop():
+        print(pipeline.smoothed_decision(batch_data))
+    

+ 40 - 0
backend/tests/core/mi/test_wpli.py

@@ -0,0 +1,40 @@
+""" WPLI 单元测试 """
+import os
+from core.sig_chain.sig_reader import Reader
+from core.mi.eeg_wpli import Wpli
+
+TEST_DATA_PATH = "tests/data/"
+BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, "eeg_raw_data.bdf")
+WPLI_FILE_PATH = os.path.join(TEST_DATA_PATH, "wpli.png")
+
+
+def setup_module():
+    if not os.path.exists(TEST_DATA_PATH):
+        os.makedirs(TEST_DATA_PATH)
+
+
+def teardown_module():
+    if os.path.exists(WPLI_FILE_PATH):
+        os.remove(WPLI_FILE_PATH)
+
+
+def test_main():
+    # WPLI/CSP
+    # 左右手
+    ch_names = [
+        "Fz", "Fp1", "F3", "F7", "C3", "T3", "T5", "P3", "O1", "Cz", "Oz", "Pz",
+        "O2", "P4", "T6", "T4", "C4", "F8", "F4", "Fp2"
+    ]
+
+    reader = Reader()
+    raw = reader.read(BDF_FILE_PATH, tuple(ch_names))
+    raw.annotations.rename({
+        "trainSuccess": "mi",
+        "trainFailed": "mi",
+        "restState": "rest"
+    })
+
+    wpli = Wpli()
+    _, mi_epochs, _ = wpli.get_epochs(raw, tuple(ch_names))
+    con_wpli, con_wpli_data = wpli.process(mi_epochs, raw.info["sfreq"])
+    wpli.draw_image(con_wpli, con_wpli_data, save_path=WPLI_FILE_PATH)

+ 68 - 0
backend/tests/core/peripheral/hand/test_fubo_pneumatic_finger.py

@@ -0,0 +1,68 @@
+'''
+@Author  :   liujunshen
+@File    :   test_fubo_pneumatic_finger.py
+@Time    :   2023/04/04 17:13:01
+富伯气动手测试用例,需要: 1.连接富伯气动手 2.开机 3.进入镜像模式 4.启动 5.获取串口名称并修改
+'''
+
+import time
+
+import pytest
+
+from core.peripheral.hand.fubo_pneumatic_finger import FuboPneumaticFingerClient
+from core.peripheral.hand.fubo_pneumatic_finger import get_serial_ports
+
+PORT = "COM4"
+init_params = {"port": PORT}
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_get_ports_from_computer_success():
+    ports = get_serial_ports()
+    assert len(ports) > 0
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_client_init_success():
+    client = FuboPneumaticFingerClient(init_params)
+    ret = client.init()
+    assert ret["is_connected"]
+    client.close()
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_client_close_success():
+    client = FuboPneumaticFingerClient(init_params)
+    client.init()
+    ret = client.close()
+    assert not ret["is_connected"]
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_start_flex_success():
+    client = FuboPneumaticFingerClient(init_params)
+    client.init()
+    receive = client.flex()
+    assert len(receive) > 0
+    time.sleep(3)
+    client.close()
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_start_extend_success():
+    client = FuboPneumaticFingerClient(init_params)
+    client.init()
+    receive = client.extend()
+    assert len(receive) > 0
+    time.sleep(3)
+    client.close()
+
+
+@pytest.mark.fubo_pneumatic_finger
+def test_start_operate_success():
+    client = FuboPneumaticFingerClient(init_params)
+    client.init()
+    receive = client.start()
+    assert len(receive) > 0
+    time.sleep(15)
+    client.close()

+ 274 - 0
backend/tests/core/peripheral/hand/test_ruishou.py

@@ -0,0 +1,274 @@
+"""
+@Author  :   liujunshen
+@File    :   test_ruishou.py
+@Time    :   2023/04/04 13:57:03
+"""
+
+from collections import namedtuple
+import time
+
+import pytest
+
+from core.peripheral.hand.ruishou import Constants
+from core.peripheral.hand.ruishou import Protocol
+from core.peripheral.hand.ruishou import RuishouClient
+from core.peripheral.hand.ruishou import RuishouConnector
+
+buffer_time = 0.3
+ParamStruct = namedtuple(
+    "ParamStruct",
+    "hand_select thumb index_finger middle_finger ring_finger little_finger duration"
+)
+
+
+# ============= 测试解析模块 ================
+def test_protocol_get_pack_success():
+    protocol = Protocol()
+    ret = protocol.get_pck("finish_action")
+    assert isinstance(ret, bytearray)
+
+
+def test_protocol_get_pack_with_fail_cmd_return_none():
+    protocol = Protocol()
+    ret = protocol.get_pck("error_cmd")
+    assert ret is None
+
+
+def test_protocol_get_motion_control_pack_success():
+    params = {
+        Constants.SendPckLocation.MOTION_CONTROL_HAND: 2,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_INDEX_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_MIDDLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 10
+    }
+    protocol = Protocol()
+    ret = protocol.get_pck("motion_control", params)
+    assert isinstance(ret, bytearray)
+
+
+def test_protocol_get_motion_control_pack_with_lack_update_dict_return_none():
+    params = {
+        Constants.SendPckLocation.MOTION_CONTROL_HAND: 2,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 10,
+    }
+    protocol = Protocol()
+    ret = protocol.get_pck("motion_control", params)
+    assert ret is None
+
+
+def test_protocol_get_motion_control_pack_with_error_update_dict_return_none():
+    params = {
+        11: buffer_time,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 10,
+    }
+    protocol = Protocol()
+    ret = protocol.get_pck("motion_control", params)
+    assert ret is None
+
+
+def test_protocol_unpack_one_cmd_bytes_success():
+    protocol = Protocol()
+    b = b"\xae\xaf\x05\x01\x00\x00\xff\xff\x01\xff"
+    ret = protocol.unpack_bytes(b)
+    assert isinstance(ret, list)
+    assert len(ret) == 1
+
+
+def test_protocol_unpack_two_cmd_bytes_success():
+    protocol = Protocol()
+    b = b"\xae\xaf\x05\x01\x00\x00\xff\xff\x01\xff\xae\xaf\x05\x02\xff\xff\xff\xff\x03\xfe"
+    ret = protocol.unpack_bytes(b)
+    assert isinstance(ret, list)
+    assert len(ret) == 2
+
+
+def test_protocol_unpack_bytes_with_error_bytes_return_empty_list():
+    protocol = Protocol()
+    b = b"\x05\x01\x00\x00\xff\xff\x01\xff"
+    ret = protocol.unpack_bytes(b)
+    assert isinstance(ret, list)
+    assert len(ret) == 0
+
+
+def test_protocol_parse_list_success():
+    protocol = Protocol()
+    b = b"\xae\xaf\x05\x01\x00\x00\xff\xff"
+    unpack_data = protocol.unpack_bytes(b)
+    parsed_data = protocol.parse_bytes(unpack_data[0])
+    assert isinstance(parsed_data, dict)
+
+
+# =======以下需要启动设备或模拟测试软件=============
+
+
+@pytest.mark.ruishou
+def test_connector_connect_and_close_success():
+    connector = RuishouConnector()
+    connector.start_client()
+    time.sleep(buffer_time)
+    connector.close_client()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_connector_sync_send_control_motion_data_success():
+    connector = RuishouConnector()
+    connector.start_client()
+    params = {
+        Constants.SendPckLocation.MOTION_CONTROL_HAND: 2,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_INDEX_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_MIDDLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 5
+    }
+    res = connector.sync_send_data("motion_control", params)
+    assert isinstance(res, dict)
+    time.sleep(5)
+    connector.close_client()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_connector_sync_send_error_data_return_none():
+    connector = RuishouConnector()
+    connector.start_client()
+    params = {}
+    res = connector.sync_send_data("error_data", params)
+    assert res is None
+    time.sleep(buffer_time)
+    connector.close_client()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_connector_stop_operate_success():
+    connector = RuishouConnector()
+    connector.start_client()
+    params = {
+        Constants.SendPckLocation.MOTION_CONTROL_HAND: 1,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_INDEX_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_MIDDLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 5
+    }
+    connector.sync_send_data("motion_control", params)
+    time.sleep(3)
+    connector.sync_send_data("finish_action")
+    connector.close_client()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_connector_sync_send_control_motion_error_params_fail():
+    connector = RuishouConnector()
+    connector.start_client()
+    params = {
+        Constants.SendPckLocation.MOTION_CONTROL_HAND: 2,
+        Constants.SendPckLocation.MOTION_CONTROL_THUMB_BENDING: 15,
+        Constants.SendPckLocation.MOTION_CONTROL_RING_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_LITTLE_FINGER_BENDING: 10,
+        Constants.SendPckLocation.MOTION_CONTROL_DURATION: 5
+    }
+    res = connector.sync_send_data("motion_control", params)
+    assert res is None
+    time.sleep(buffer_time)
+    connector.close_client()
+    time.sleep(buffer_time)
+
+
+# ========== 测试睿手客户端(对业务) ============
+
+
+@pytest.mark.ruishou
+def test_client_init_success():
+    client = RuishouClient()
+    ret = client.init()
+    assert ret["is_connected"]
+    time.sleep(buffer_time)
+    client.close()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_get_status_success():
+    client = RuishouClient()
+    client.init()
+    status = client.status()
+    assert status["is_connected"]
+    time.sleep(buffer_time)
+    client.close()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_get_status_with_not_init_return_not_connected():
+    client = RuishouClient()
+    status = client.status()
+    assert not status["is_connected"]
+
+
+@pytest.mark.ruishou
+def test_client_get_status_with_close_client_return_not_connected():
+    client = RuishouClient()
+    client.init()
+    time.sleep(buffer_time)
+    client.close()
+    status = client.status()
+    assert not status["is_connected"]
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_start_operate_success():
+    client = RuishouClient()
+    client.init()
+    params = ParamStruct("左手", 10, 10, 10, 10, 10, 6)
+    res = client._control_motion(params)
+    assert isinstance(res, dict)
+    time.sleep(5)
+    client.close()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_reconnect_start_operate_success():
+    client = RuishouClient()
+    client.init()
+    params = ParamStruct("左手", 10, 10, 10, 10, 10, 6)
+    time.sleep(buffer_time)
+    client.close()
+    res = client._control_motion(params)
+    assert isinstance(res, dict)
+    time.sleep(5)
+    client.close()
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_close_success():
+    client = RuishouClient()
+    client.init()
+    time.sleep(buffer_time)
+    client.close()
+    assert not client.connector.is_connected
+    time.sleep(buffer_time)
+
+
+@pytest.mark.ruishou
+def test_client_close_with_not_init_success():
+    client = RuishouClient()
+    client.close()
+    assert not client.connector.is_connected

+ 171 - 0
backend/tests/core/sig_chain/device/test_faker.py

@@ -0,0 +1,171 @@
+"""Module tests/core/sig_chain/device/test_neo provide test for neo connector"""
+import pytest
+import struct
+import time
+import unittest
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import numpy as np
+
+from core.sig_chain.device.faker import FakerConnector
+
+
+TASK_PER_RUN = 1
+
+
+def teardown_function():
+    FakerConnector.clear_instance()
+
+
+def gen_fake_recv_bytes(data_count_per_channel, channel_count):
+    recv_timestamp_byte = struct.pack('d', time.time())
+    # 假的接收数据(每行是一个通道)
+    # 例如 2通道 x 3点:
+    # [[1, 1, 1],
+    #  [2, 2, 2]]
+    recv_data = np.ones((channel_count, data_count_per_channel),
+                        dtype=np.float32)
+    for ii in range(channel_count):
+        recv_data[ii, :] = (ii + 1) * recv_data[ii, :]
+
+    recv_bytes =  recv_timestamp_byte + recv_data.tobytes()
+    return recv_bytes, recv_data
+
+
+# ===================
+
+
+def test_new_connector_is_disconnected():
+    connector = FakerConnector()
+    assert not connector.is_connected()
+
+
+def test_new_connector_receive_wave_failed():
+    connector = FakerConnector()
+    with pytest.raises(Exception):
+        connector.receive_wave()
+
+
+def test_after_get_ready_is_connected():
+    connector = FakerConnector()
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    with patch('socket.socket', mock_socket):
+        success = connector.get_ready()
+        assert success
+    assert connector.is_connected()
+
+
+@unittest.skip('未实现')
+def test_after_get_ready_skip_connect_request():
+    connector = FakerConnector()
+    connector.get_ready()
+    success = connector.get_ready()
+    assert success
+
+
+def test_after_connected_receive_wave_success():
+    connector = FakerConnector()
+
+    recv_bytes, recv_data = gen_fake_recv_bytes(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    def side_effect(arg): # 用于确认接收到的参数
+        assert (arg == recv_data).all()
+    connector._add_a_data_block_to_buffer = MagicMock(side_effect=side_effect)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_bytes  #b''
+    connector._sock = mock_socket
+
+    success = connector.receive_wave()
+    assert success
+
+
+def test_after_stop_is_disconnected():
+    connector = FakerConnector()
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.close = MagicMock()
+    with patch('socket.socket', mock_socket):
+        connector.get_ready()
+        connector.stop()
+    assert not connector.is_connected()
+
+
+def test_load_partial_config_success():
+    connector = FakerConnector()
+    mock_config = {
+        'host': '1.0.0.1'
+    }
+    connector.load_config(mock_config)
+    assert connector._host == mock_config['host']
+
+
+def test_after_set_saver_buffer_is_set():
+    connector = FakerConnector()
+    connector.set_saver()
+
+    assert connector.buffer_save is not None
+
+
+def test_before_set_edf_header_save_data_not_called():
+    connector = FakerConnector()
+    connector.set_saver()
+
+    mock_save_raw_data = MagicMock()
+    connector.saver.save_raw_data = mock_save_raw_data
+
+    recv_timestamp_byte = struct.pack('d', time.time())
+    recv_bytes, _ = gen_fake_recv_bytes(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_bytes
+    connector._sock = mock_socket
+    connector.receive_wave()
+    assert not mock_save_raw_data.called
+
+
+def test_after_receive_wave_observers_are_notified():
+    connector = FakerConnector()
+
+    recv_bytes, _ = gen_fake_recv_bytes(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_bytes
+    connector._sock = mock_socket
+    connector._save_data_when_buffer_full = MagicMock()
+
+    mock_notify_observers = MagicMock()
+    connector.notify_observers = mock_notify_observers
+
+    connector.receive_wave()
+    assert mock_notify_observers.called
+
+
+def test_main():
+    # pylint: disable=import-outside-toplevel
+    from schemas.subjects import SubjectCreate
+    # pylint: enable=import-outside-toplevel
+    connector = FakerConnector()
+    connector.set_saver()
+    subject = SubjectCreate(name='nobody',
+                            id_card='12345',
+                            gender='男',
+                            birthday='1988-01-01',
+                            rehabilitation_parts=['左手'])
+    connector.saver.set_edf_header(subject, 'filename.bdf', TASK_PER_RUN, '.')
+    if connector.get_ready():
+        for _ in range(20):
+            connector.receive_wave()
+    connector.stop()

+ 186 - 0
backend/tests/core/sig_chain/device/test_neo.py

@@ -0,0 +1,186 @@
+"""Module tests/core/sig_chain/device/test_neo provide test for neo connector"""
+import pytest
+import struct
+import unittest
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import numpy as np
+
+from core.sig_chain.device.neo import bytes_to_float32
+from core.sig_chain.device.neo import NeoConnector
+
+
+TASK_PER_RUN = 1
+
+
+def teardown_function():
+    NeoConnector.clear_instance()
+
+
+def gen_fake_recv_data(data_count_per_channel, channel_count):
+    # 假的接收数据
+    recv_data = np.ones((data_count_per_channel, channel_count),
+                        dtype=np.float32)
+    for ii in range(channel_count):
+        recv_data[:, ii] = (ii + 1) * recv_data[:, ii]
+    return recv_data
+
+
+# ===================
+
+
+def test_new_connector_is_disconnected():
+    connector = NeoConnector()
+    assert not connector.is_connected()
+
+
+def test_new_connector_receive_wave_failed():
+    connector = NeoConnector()
+    with pytest.raises(Exception):
+        connector.receive_wave()
+
+
+def test_after_get_ready_is_connected():
+    connector = NeoConnector()
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    with patch('socket.socket', mock_socket):
+        success = connector.get_ready()
+        assert success
+    assert connector.is_connected()
+
+
+@unittest.skip('未实现')
+def test_after_get_ready_skip_connect_request():
+    connector = NeoConnector()
+    connector.get_ready()
+    success = connector.get_ready()
+    assert success
+
+
+def test_after_connected_receive_wave_success():
+    connector = NeoConnector()
+
+    recv_data = gen_fake_recv_data(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    def side_effect(arg): # 用于确认接收到的参数
+        assert (arg == recv_data.T).all()
+    connector._add_a_data_block_to_buffer = MagicMock(side_effect=side_effect)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_data.tobytes() #b''
+    connector._sock = mock_socket
+
+    success = connector.receive_wave()
+    assert success
+
+
+def test_after_stop_is_disconnected():
+    connector = NeoConnector()
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.close = MagicMock()
+    with patch('socket.socket', mock_socket):
+        connector.get_ready()
+        connector.stop()
+    assert not connector.is_connected()
+
+
+def test_load_partial_config_success():
+    connector = NeoConnector()
+    mock_config = {
+        'host': '1.0.0.1'
+    }
+    connector.load_config(mock_config)
+    assert connector._host == mock_config['host']
+
+
+def test_after_set_saver_buffer_is_set():
+    connector = NeoConnector()
+    connector.set_saver()
+
+    assert connector.buffer_save is not None
+
+
+def test_before_set_edf_header_save_data_not_called():
+    connector = NeoConnector()
+    connector.set_saver()
+
+    mock_save_raw_data = MagicMock()
+    connector.saver.save_raw_data = mock_save_raw_data
+
+    recv_data = gen_fake_recv_data(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_data.tobytes()
+    connector._sock = mock_socket
+    connector.receive_wave()
+    assert not mock_save_raw_data.called
+
+
+def test_after_receive_wave_observers_are_notified():
+    connector = NeoConnector()
+
+    recv_data = gen_fake_recv_data(
+        connector.sample_params.data_count_per_channel,
+        connector.sample_params.channel_count)
+
+    mock_socket = MagicMock()
+    mock_socket.connect.return_value = True
+    mock_socket.recv.return_value = recv_data.tobytes()
+    connector._sock = mock_socket
+    connector._save_data_when_buffer_full = MagicMock()
+
+    mock_notify_observers = MagicMock()
+    connector.notify_observers = mock_notify_observers
+
+    connector.receive_wave()
+    assert mock_notify_observers.called
+
+
+def test_with_matched_packet_bytes_to_float32_success():
+    expected = [12.0, 0.0, -12398.1982421875, 34567.98828125]
+    packet = b''
+    for value in expected:
+        packet += struct.pack('f', value)
+
+    result = bytes_to_float32(packet, len(packet), 4)
+
+    assert expected == result
+
+
+def test_mismatched_packet_bytes_to_float32_failed():
+    expected = [12.0, 0.0, -12398.1982421875, 34567.98828125]
+    packet = b''
+    for value in expected:
+        packet += struct.pack('f', value)
+    packet = packet[:-2]
+
+    with pytest.raises(AssertionError):
+        bytes_to_float32(packet, len(packet), 4)
+
+
+def test_main():
+    # pylint: disable=import-outside-toplevel
+    from schemas.subjects import SubjectCreate
+    # pylint: enable=import-outside-toplevel
+    connector = NeoConnector()
+    connector.set_saver()
+    subject = SubjectCreate(name='nobody',
+                            id_card='12345',
+                            gender='男',
+                            birthday='1988-01-01',
+                            rehabilitation_parts=['左手'])
+    connector.saver.set_edf_header(subject, 'filename.bdf', TASK_PER_RUN, '.')
+    if connector.get_ready():
+        for _ in range(20):
+            connector.receive_wave()
+    connector.stop()

+ 580 - 0
backend/tests/core/sig_chain/test_pre_process.py

@@ -0,0 +1,580 @@
+"""单元测试预处理,通过图来看,没有做断言 """
+#pylint: disable=protected-access
+import unittest
+from unittest.mock import MagicMock
+
+import matplotlib.pyplot as plt
+import mne
+import numpy as np
+import pytest
+from scipy import signal
+
+from core.sig_chain.pre_process import PreProcessor
+from core.sig_chain.pre_process import RealTimeFilter
+from core.sig_chain.pre_process import RealTimeFilterM
+
+
+sampling_freq = 1000
+times = np.linspace(0, 1, sampling_freq, endpoint=False)
+sine = np.sin(2 * np.pi * times)
+cosine = np.cos(2 * np.pi * times)
+data = np.array([sine, cosine])
+info = mne.create_info(ch_names=["C3", "C4"],
+                       ch_types=["eeg"] * 2,
+                       sfreq=sampling_freq)
+
+
+raw_data = mne.io.RawArray(data, info)
+
+
+@unittest.skip("通过看图确认结果")
+def test_detrend_all():
+    x1 = PreProcessor.detrend(raw_data)
+    x2 = PreProcessor.detrend(raw_data)
+    x3 = PreProcessor.detrend(raw_data)
+    x4 = PreProcessor.detrend(raw_data)
+    x5 = PreProcessor.detrend(raw_data)
+    signals = np.concatenate((x1.get_data(), x2.get_data(), x3.get_data(),
+                              x4.get_data(), x5.get_data()),
+                             axis=1)
+    result = mne.io.RawArray(signals, info)
+    result.plot(scalings="auto")
+    print("finish")
+
+
+@unittest.skip("通过看图确认结果")
+def test_detrend_and_resample_all():
+    x1 = PreProcessor.detrend(raw_data)
+    x1 = PreProcessor.resample_direct(x1, 250)
+    x2 = PreProcessor.detrend(raw_data)
+    x2 = PreProcessor.resample_direct(x2, 250)
+    x3 = PreProcessor.detrend(raw_data)
+    x3 = PreProcessor.resample_direct(x3, 250)
+    x4 = PreProcessor.detrend(raw_data)
+    x4 = PreProcessor.resample_direct(x4, 250)
+    x5 = PreProcessor.detrend(raw_data)
+    x5 = PreProcessor.resample_direct(x5, 250)
+    signals = np.concatenate((x1.get_data(), x2.get_data(), x3.get_data(),
+                              x4.get_data(), x5.get_data()),
+                             axis=1)
+    result = mne.io.RawArray(signals, info)
+    result.plot(scalings="auto")
+    print("finish")
+
+
+@unittest.skip("通过看图确认结果")
+def test_resample_and_detrend_all():
+    x1 = PreProcessor.resample_direct(raw_data, 250)
+    x1 = PreProcessor.detrend(x1)
+    x2 = PreProcessor.resample_direct(raw_data, 250)
+    x2 = PreProcessor.detrend(x2)
+    x3 = PreProcessor.resample_direct(raw_data, 250)
+    x3 = PreProcessor.detrend(x3)
+    x4 = PreProcessor.resample_direct(raw_data, 250)
+    x4 = PreProcessor.detrend(x4)
+    x5 = PreProcessor.resample_direct(raw_data, 250)
+    x5 = PreProcessor.detrend(x5)
+    signals = np.concatenate((x1.get_data(), x2.get_data(), x3.get_data(),
+                              x4.get_data(), x5.get_data()),
+                             axis=1)
+    result = mne.io.RawArray(signals, info)
+    result.plot(scalings="auto")
+    print("finish")
+
+
+class TestRealTimeFilter:
+    """单通道实时滤波器测试 """
+    def generate_signal(self, high_freq=10, low_freq=0.4):
+        # 生成信号:10Hz的正弦波 + 0.4Hz的正弦波
+        tt = np.linspace(0, 1, 1000, endpoint=False)
+        xx = np.sin(2 * np.pi * high_freq * tt) + np.sin(
+            2 * np.pi * low_freq * tt)
+        return tt, xx
+
+    def plot_to_compare(self, tt, xx, yy, ref_yy):
+        # 绘制滤波前后的信号
+        plt.subplot(3, 1, 1)
+        plt.plot(tt, xx)
+        plt.title("Original Signal")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.subplot(3, 1, 2)
+        plt.plot(tt, yy)
+        plt.title("Filtered Signal")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.subplot(3, 1, 3)
+        plt.plot(tt, ref_yy)
+        plt.title("Filtered Signal(ref)")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.tight_layout()
+        plt.show()
+        print("end")
+
+    def test_cal_weighted_sum_x(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+        for pos_x in range(3):
+            rt_filter._buffer_x = [0, 1, 2]
+            # rt_filter._buffer_y = [0, 1, 2]
+            rt_filter._pos_x = pos_x
+
+            factor_b = np.array(bb)
+            factor_x = np.array(
+                rt_filter._buffer_x[:rt_filter._pos_x + 1][::-1] +
+                rt_filter._buffer_x[rt_filter._pos_x + 1:][::-1])
+            expected = np.dot(factor_b, factor_x)
+            weighted_sum_x = rt_filter.cal_weighted_sum_x()
+
+            assert expected == weighted_sum_x
+
+    def test_cal_weighted_sum_y(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+        for pos_y in range(3):
+            # rt_filter._buffer_x = [0, 1, 2]
+            rt_filter._buffer_y = [0, 1, 2]
+            rt_filter._pos_y = pos_y
+
+            factor_a = np.array(aa[1:])
+            factor_y = np.array(
+                rt_filter._buffer_y[:rt_filter._pos_y][::-1] +
+                rt_filter._buffer_y[rt_filter._pos_y + 1:][::-1])
+            expected = np.dot(factor_a, factor_y)
+            weighted_sum_y = rt_filter.cal_weighted_sum_y()
+
+            assert expected == weighted_sum_y
+
+    def test_filter_update_xn_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+
+        for pos_x in range(3):
+            rt_filter.cal_weighted_sum_x = MagicMock(return_value=1.0)
+            rt_filter.cal_weighted_sum_y = MagicMock(return_value=2.0)
+            rt_filter._pos_x = pos_x
+
+            rt_filter.filter(1.0)
+
+            assert 1.0 == rt_filter._buffer_x[pos_x]
+
+    def test_filter_update_yn_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+
+        for pos_y in range(3):
+            rt_filter.cal_weighted_sum_x = MagicMock(return_value=1.0)
+            rt_filter.cal_weighted_sum_y = MagicMock(return_value=2.0)
+            rt_filter._pos_y = pos_y
+
+            yn = rt_filter.filter(1.0)
+
+            assert -1.0 == yn
+            assert yn == rt_filter._buffer_y[pos_y]
+
+    def test_filter_update_pos_x_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+        expected_pos_xs = [1,2,0]
+        for pos_x, expected_pos_x in zip(range(3), expected_pos_xs):
+            rt_filter.cal_weighted_sum_x = MagicMock(return_value=1.0)
+            rt_filter.cal_weighted_sum_y = MagicMock(return_value=2.0)
+            rt_filter._pos_x = pos_x
+
+            rt_filter.filter(1.0)
+
+            assert expected_pos_x == rt_filter._pos_x
+
+    def test_filter_update_pos_y_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilter(aa, bb)
+        expected_pos_ys = [1,2,0]
+        for pos_y, expected_pos_y in zip(range(3), expected_pos_ys):
+            rt_filter.cal_weighted_sum_x = MagicMock(return_value=1.0)
+            rt_filter.cal_weighted_sum_y = MagicMock(return_value=2.0)
+            rt_filter._pos_y = pos_y
+
+            rt_filter.filter(1.0)
+
+            assert expected_pos_y == rt_filter._pos_y
+
+    def test_init_eeg_with_invalid_code_raise_exc(self):
+        with pytest.raises(AssertionError):
+            RealTimeFilter.init_eeg(3)
+
+    def test_init_eeg_with_valid_code_success(self):
+        # butter 高通0.5Hz
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+
+        # 生成信号
+        tt, xx = self.generate_signal()
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx)
+
+        # us
+        rt_filter = RealTimeFilter.init_eeg(0)
+        yy = []
+        for xn in xx:
+            yy.append(rt_filter.filter(xn))
+
+        self.plot_to_compare(tt, xx, yy, ref_yy)
+
+    @unittest.skip("结果错误")
+    @pytest.mark.pp_manual
+    def test_butter_high_pass_fixed(self):
+        # butter 高通0.5Hz
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+
+        # 生成信号
+        tt, xx = self.generate_signal()
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx)
+
+        # us
+        rt_filter = RealTimeFilter(aa, bb)
+        yy = []
+        for xn in xx:
+            yy.append(rt_filter.filter(xn))
+
+        self.plot_to_compare(tt, xx, yy, ref_yy)
+
+    @pytest.mark.pp_manual
+    def test_butter_low_pass_fixed(self):
+        # 60Hz低通
+        # aa = [1, -0.031426266043351]
+        # bb = [0.484286866978324, 0.484286866978324]
+        aa = [1, -0.290526856731916]
+        bb = [0.354736571634042, 0.354736571634042]
+
+        # 生成信号
+        tt, xx = self.generate_signal(220, 0.5)
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx)
+
+        # us
+        rt_filter = RealTimeFilter(aa, bb)
+        yy = []
+        for xn in xx:
+            yy.append(rt_filter.filter(xn))
+
+        self.plot_to_compare(tt, xx, yy, ref_yy)
+
+    @pytest.mark.pp_manual
+    def test_butter_high_pass_scipy(self):
+        # butter 高通0.5Hz
+        freq = 0.5
+        fs = 1000
+        bb, aa = signal.butter(2, [2*freq/fs], "hp")
+
+        # w, h = signal.freqz(bb, aa, worN=np.logspace(-1, 2, 1000))
+        # plt.semilogx(w, 20 * np.log10(abs(h)))
+        # plt.xlabel("Frequency")
+        # plt.ylabel("Amplitude response [dB]")
+        # plt.grid(True)
+        # plt.show()
+
+        # 生成信号
+        tt, xx = self.generate_signal()
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx)
+
+        # us
+        rt_filter = RealTimeFilter(aa, bb)
+        yy = []
+        for xn in xx:
+            yy.append(rt_filter.filter(xn))
+
+        self.plot_to_compare(tt, xx, yy, ref_yy)
+
+    @pytest.mark.pp_manual
+    def test_butter_low_pass_scipy(self):
+        # 60Hz低通
+        freq = 60
+        fs = 1000
+        bb, aa = signal.butter(1, [2*freq/fs])
+
+        # 生成信号
+        tt, xx = self.generate_signal(120, 0.5)
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx)
+
+        # us
+        rt_filter = RealTimeFilter(aa, bb)
+        yy = []
+        for xn in xx:
+            yy.append(rt_filter.filter(xn))
+
+        self.plot_to_compare(tt, xx, yy, ref_yy)
+
+class TestRealTimeFilterM:
+    """多通道实时滤波器测试 """
+
+    def generate_signal(self, high_freqs, low_freqs):
+        # 生成信号
+        tt = np.linspace(0, 1, 1000, endpoint=False)
+        #10Hz的正弦波 + 0.4Hz的正弦波
+        x1 = np.sin(2 * np.pi * high_freqs[0] * tt) + np.sin(
+            2 * np.pi * low_freqs[0] * tt)
+        #2Hz的正弦波 + 0.3Hz的正弦波
+        x2 = np.sin(2 * np.pi * high_freqs[1] * tt) + np.sin(
+            2 * np.pi * low_freqs[1] * tt)
+        xx = np.stack([x1, x2], axis=0)
+        return tt, xx
+
+    def plot_to_compare(self, tt, xx, yy, ref_yy, selected_channel):
+        # 绘制滤波前后的信号
+        plt.subplot(3, 1, 1)
+        plt.plot(tt, xx[selected_channel, :])
+        plt.title("Original Signal")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.subplot(3, 1, 2)
+        plt.plot(tt, yy)
+        plt.title("Filtered Signal")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.subplot(3, 1, 3)
+        plt.plot(tt, ref_yy)
+        plt.title("Filtered Signal(ref)")
+        plt.xlabel("Time (s)")
+        plt.ylabel("Amplitude")
+        plt.grid(True)
+
+        plt.tight_layout()
+        plt.show()
+
+    def test_cal_weighted_sum_x(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+        for pos_x in range(3):
+            rt_filter._buffer_x = np.array([[0, 1, 2], [2, 1, 5]])
+            rt_filter._pos_x = pos_x
+
+            factor_b = np.array(bb)
+            factor_x = np.concatenate([
+                rt_filter._buffer_x[:, :rt_filter._pos_x + 1][:, ::-1],
+                rt_filter._buffer_x[:, rt_filter._pos_x + 1:][:, ::-1]
+            ], axis=1)
+            expected = np.sum(factor_b * factor_x, axis=1)
+            weighted_sum_x = rt_filter.cal_weighted_sum_x()
+
+            assert (expected == weighted_sum_x).all()
+
+    def test_cal_weighted_sum_y(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+        for pos_y in range(3):
+            rt_filter._buffer_y = np.array([[0, 1, 2], [2, 1, 5]])
+            rt_filter._pos_y = pos_y
+
+            factor_a = np.array(aa[1:])
+            factor_y = np.concatenate([
+                rt_filter._buffer_y[:, :rt_filter._pos_y][:, ::-1],
+                rt_filter._buffer_y[:, rt_filter._pos_y + 1:][:, ::-1]
+            ], axis=1)
+            expected = np.sum(factor_a * factor_y, axis=1)
+            weighted_sum_y = rt_filter.cal_weighted_sum_y()
+
+            assert ( expected == weighted_sum_y ).all()
+
+    def test_filter_update_xn_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+
+        for pos_x in range(3):
+            rt_filter.cal_weighted_sum_x = MagicMock(
+                return_value=np.array([1.0, 2.0]))
+            rt_filter.cal_weighted_sum_y = MagicMock(
+                return_value=np.array([2.0, 5.0]))
+            rt_filter._pos_x = pos_x
+
+            xn = np.array([1.0, 2.0])
+            rt_filter.filter(xn)
+
+            assert (xn == rt_filter._buffer_x[:, pos_x]).all()
+
+    def test_filter_update_yn_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+
+        for pos_y in range(3):
+            rt_filter.cal_weighted_sum_x = MagicMock(
+                return_value=np.array([1.0, 2.0]))
+            rt_filter.cal_weighted_sum_y = MagicMock(
+                return_value=np.array([2.0, 5.0]))
+            rt_filter._pos_y = pos_y
+
+            yn = rt_filter.filter(1.0)
+
+            assert (np.array([-1.0, -3.0]) == yn).all()
+            assert (yn == rt_filter._buffer_y[:, pos_y]).all()
+
+    def test_filter_update_pos_x_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+        expected_pos_xs = [1,2,0]
+        for pos_x, expected_pos_x in zip(range(3), expected_pos_xs):
+            rt_filter.cal_weighted_sum_x = MagicMock(
+                return_value=np.array([1.0, 2.0]))
+            rt_filter.cal_weighted_sum_y = MagicMock(
+                return_value=np.array([2.0, 5.0]))
+            rt_filter._pos_x = pos_x
+
+            rt_filter.filter(1.0)
+
+            assert expected_pos_x == rt_filter._pos_x
+
+    def test_filter_update_pos_y_correct(self):
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+        rt_filter = RealTimeFilterM(aa, bb, 2)
+        expected_pos_ys = [1,2,0]
+        for pos_y, expected_pos_y in zip(range(3), expected_pos_ys):
+            rt_filter.cal_weighted_sum_x = MagicMock(
+                return_value=np.array([1.0, 2.0]))
+            rt_filter.cal_weighted_sum_y = MagicMock(
+                return_value=np.array([2.0, 5.0]))
+            rt_filter._pos_y = pos_y
+
+            rt_filter.filter(1.0)
+
+            assert expected_pos_y == rt_filter._pos_y
+
+    def test_init_eeg_with_invalid_code_raise_exc(self):
+        with pytest.raises(AssertionError):
+            RealTimeFilterM.init_eeg(3, 24)
+
+    def test_init_eeg_with_valid_code_success(self):
+        # butter 高通0.5Hz
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+
+        # 生成信号
+        tt, xx = self.generate_signal([10, 2], [0.4, 0.3])
+        selected_channel = 0
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx[selected_channel, :])
+
+        # us
+        channel, point_count = xx.shape
+        rt_filter = RealTimeFilterM.init_eeg(0, channel)
+        m_yy = np.zeros_like(xx, dtype=np.float64)
+        for ii in range(point_count):
+            xn = xx[:, ii]
+            m_yy[:, ii] = rt_filter.filter(xn)
+        yy = m_yy[selected_channel, :]
+
+        self.plot_to_compare(tt, xx, yy, ref_yy, selected_channel)
+
+    @pytest.mark.pp_manual
+    def test_butter_high_pass_fixed(self):
+        # butter 高通0.5Hz
+        aa = [1, -1.982228929792529, 0.982385450614125]
+        bb = [0.991153595101663, -1.982307190203327, 0.991153595101663]
+
+        # 生成信号
+        tt, xx = self.generate_signal([10, 2], [0.4, 0.3])
+        selected_channel = 0
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx[selected_channel, :])
+
+        # us
+        channel, point_count = xx.shape
+        rt_filter = RealTimeFilterM(aa, bb, channel)
+        m_yy = np.zeros_like(xx, dtype=np.float64)
+        for ii in range(point_count):
+            xn = xx[:, ii]
+            m_yy[:, ii] = rt_filter.filter(xn)
+        yy = m_yy[selected_channel, :]
+
+        self.plot_to_compare(tt, xx, yy, ref_yy, selected_channel)
+
+    @pytest.mark.pp_manual
+    def test_butter_high_pass_scipy(self):
+        # butter 高通0.5Hz
+        freq = 0.5
+        fs = 1000
+        bb, aa = signal.butter(2, [2*freq/fs], "hp")
+
+        # 生成信号
+        tt, xx = self.generate_signal([10, 2], [0.4, 0.3])
+        selected_channel = 0
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx[selected_channel, :])
+
+        # us
+        channel, point_count = xx.shape
+        rt_filter = RealTimeFilterM(aa, bb, channel)
+        m_yy = np.zeros_like(xx, dtype=np.float64)
+        for ii in range(point_count):
+            xn = xx[:, ii]
+            m_yy[:, ii] = rt_filter.filter(xn)
+        yy = m_yy[selected_channel, :]
+
+        self.plot_to_compare(tt, xx, yy, ref_yy, selected_channel)
+
+    @pytest.mark.pp_manual
+    def test_butter_low_pass_scipy(self):
+        # 60Hz低通
+        freq = 60
+        fs = 1000
+        bb, aa = signal.butter(2, [2*freq/fs])
+
+        # 生成信号
+        tt, xx = self.generate_signal([120, 200], [40, 50])
+        selected_channel = 0
+
+        # 应用滤波器
+        # scipy
+        ref_yy = signal.lfilter(bb, aa, xx[selected_channel, :])
+
+        # us
+        channel, point_count = xx.shape
+        rt_filter = RealTimeFilterM(aa, bb, channel)
+        m_yy = np.zeros_like(xx, dtype=np.float64)
+        for ii in range(point_count):
+            xn = xx[:, ii]
+            m_yy[:, ii] = rt_filter.filter(xn)
+        yy = m_yy[selected_channel, :]
+
+        self.plot_to_compare(tt, xx, yy, ref_yy, selected_channel)

+ 223 - 0
backend/tests/core/sig_chain/test_receive.py

@@ -0,0 +1,223 @@
+"""Module tests/core/sig_chain/test_receive provide test for receiver"""
+import pytest
+import time
+import unittest
+from unittest import mock
+
+from func_timeout import FunctionTimedOut
+
+from core.sig_chain.device.connector_interface import DataMode
+from core.sig_chain.device.connector_interface import Device
+from core.sig_chain.sig_receive import Receiver
+
+
+
+def teardown_function():
+    Receiver.clear_instance()
+
+
+def test_new_receiver_is_not_ready():
+    receiver = Receiver()
+    assert not receiver.is_ready
+
+
+def test_before_select_can_not_setup_connector():
+    receiver = Receiver()
+    with pytest.raises(AssertionError):
+        receiver.setup_connector()
+
+
+def test_before_setup_connector_receive_data_failed():
+    receiver = Receiver()
+    with pytest.raises(AssertionError):
+        receiver.start_receive_wave()
+
+
+def test_before_setup_connector_get_data_from_buffer_failed():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.buffer_plot.get_sig = \
+        mock.MagicMock(return_value={'status': 'ok'})
+    with pytest.raises(RuntimeError):
+        receiver.get_data_from_buffer('plot')
+
+    receiver.buffer_classify_online.get_sig = \
+        mock.MagicMock(return_value={'status': 'ok'})
+    with pytest.raises(RuntimeError):
+        receiver.get_data_from_buffer('classify_online')
+
+
+def test_before_setup_connector_stop_receive_pass():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+    receiver.stop_receive()
+
+
+def test_after_setup_connector_is_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.get_ready = mock.MagicMock(return_value=True)
+    receiver.setup_connector()
+    assert receiver.is_ready
+
+
+def test_after_setup_wave_receive_mode_is_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.setup_wave_mode = mock.MagicMock(return_value=True)
+    receiver.setup_receive_mode(DataMode.WAVE)
+    assert receiver.is_ready
+
+
+def test_after_setup_impedance_receive_mode_is_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.setup_impedance_mode = \
+        mock.MagicMock(return_value=True)
+    receiver.setup_receive_mode(DataMode.IMPEDANCE)
+    assert receiver.is_ready
+
+
+def test_failed_setup_wave_receive_mode_is_not_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.setup_wave_mode = mock.MagicMock(return_value=False)
+    receiver.setup_receive_mode(DataMode.WAVE)
+    assert not receiver.is_ready
+
+
+def test_failed_setup_impedance_receive_mode_is_not_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.setup_impedance_mode = mock.MagicMock(return_value=False)
+    receiver.setup_receive_mode(DataMode.IMPEDANCE)
+    assert not receiver.is_ready
+
+
+def test_before_ready_stop_receive_pass():
+    receiver = Receiver()
+    receiver.stop_receive()
+
+
+def test_after_stop_receive_is_not_ready():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.connector.get_ready = mock.MagicMock(return_value=True)
+    receiver.setup_connector()
+    receiver.connector.stop = mock.MagicMock(return_value=True)
+    receiver.stop_receive()
+    assert not receiver.is_ready
+
+
+def test_receiver_singleton_keep_status():
+    receiver = Receiver()
+    receiver.is_ready = True
+    receiver = Receiver()
+    assert receiver.is_ready
+
+
+def test_change_wave_to_impedance_mode_success():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+
+    receiver.clear_all_buffer = mock.MagicMock()
+    receiver.connector.get_ready = mock.MagicMock(return_value=True)
+    receiver.setup_connector()
+
+    receiver.connector.stop = mock.MagicMock(return_value=True)
+    receiver.stop_receive()
+
+    receiver.connector.setup_impedance_mode = mock.MagicMock(return_value=True)
+    success = receiver.setup_receive_mode(DataMode.IMPEDANCE)
+    assert success
+
+
+def test_after_setup_connector_buffers_are_cleared():
+    receiver = Receiver()
+
+    mock_connector = mock.MagicMock()
+    mock_connector.get_ready.return_value = True
+    receiver.connector = mock_connector
+
+    mock_clear_all_buffer = mock.MagicMock()
+    receiver.clear_all_buffer = mock_clear_all_buffer
+
+    receiver.setup_connector()
+    assert mock_clear_all_buffer.called
+
+
+def test_after_reset_receive_mode_buffers_are_cleared():
+    receiver = Receiver()
+
+    mock_connector = mock.MagicMock()
+    mock_connector.setup_wave_mode = mock.MagicMock()
+    receiver.connector = mock_connector
+
+    mock_clear_all_buffer = mock.MagicMock()
+    receiver.clear_all_buffer = mock_clear_all_buffer
+
+    receiver.setup_receive_mode(DataMode.WAVE)
+    assert mock_clear_all_buffer.called
+
+
+def test_get_data_from_invalid_buffer_type_failed():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+    receiver.is_ready = True
+    with pytest.raises(AssertionError):
+        receiver.get_data_from_buffer('xxx')
+
+
+def test_get_data_from_buffer_success():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+    receiver.is_ready = True
+
+    mock_data = {'status': 'ok', 'data': 1}
+    receiver.buffer_plot.get_sig = \
+        mock.MagicMock(return_value=mock_data)
+
+    ret = receiver.get_data_from_buffer('plot')
+    assert ret == mock_data
+
+
+@unittest.skip('加入timeout机制会导致卡顿,因此删除此功能')
+def test_limit_time_to_get_data_from_buffer():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+    receiver.is_ready = True
+
+    receiver.buffer_plot.get_sig = \
+        mock.MagicMock(return_value={'status': 'warn'})
+
+    with pytest.raises(FunctionTimedOut):
+        receiver.get_data_from_buffer('plot')
+
+
+@unittest.skip('依赖硬件')
+def test_main():
+    receiver = Receiver()
+    receiver.select_connector(Device.NEO, 1)
+    if receiver.setup_connector():
+        receiver.start_receive_wave()
+    for _ in range(20):
+        time.sleep(1)
+        data_from_buffer = receiver.get_data_from_buffer('plot')
+        if data_from_buffer:
+            raw_data = data_from_buffer['data']
+            print(raw_data)
+    receiver.stop_receive()
+
+    receiver.setup_receive_mode(DataMode.IMPEDANCE)
+    for _ in range(20):
+        time.sleep(1)
+        impedance = receiver.receive_impedance()
+        if impedance:
+            print(impedance)

+ 143 - 0
backend/tests/core/sig_chain/test_sig_buffer.py

@@ -0,0 +1,143 @@
+"""单元测试sig_buffer"""
+import collections
+
+import numpy as np
+
+from core.sig_chain.sig_buffer import CircularBuffer
+from core.sig_chain.sig_buffer import ParserNewsetWithTime
+from core.sig_chain.sig_buffer import PaserNewsetWithoutTime
+
+TimeStamp = collections.namedtuple("Time", ["timestamp", "data"])
+data_len = 10
+package_len = 0.1
+sig_len = 0.5
+chan_labels = ["C3", "C4", "O1", "O2", "Oz"]
+chan_types = ["eeg"] * len(chan_labels)
+fs = 1000
+sig_mock = np.random.rand(len(chan_labels), int(package_len * fs))
+sig_mock_time = TimeStamp(2023, sig_mock)
+
+
+def test_update_is_success():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          PaserNewsetWithoutTime())
+    ring_len = len(ring.content)
+    ring.update(sig_mock)
+    assert len(ring.content) == ring_len + 1
+
+
+def test_ring_is_full():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          PaserNewsetWithoutTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock)
+    assert len(ring.content) == data_len / package_len
+
+
+def test_ring_get_sig():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          PaserNewsetWithoutTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock)
+    _ = ring.get_sig()
+    assert len(ring.content) == 0
+
+
+def test_enough_ring_get_data_status_is_ok():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          PaserNewsetWithoutTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock)
+    data_get = ring.get_sig()
+    status, data = data_get.values()
+    assert data.get_data().shape == (len(chan_labels), ring.data_len * fs)
+    assert status == "ok"
+
+
+def test_not_enough_ring_get_data_status_is_warn():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          PaserNewsetWithoutTime())
+    ring.update(sig_mock)
+    data_get = ring.get_sig()
+    status, data = data_get.values()
+    assert data is None
+    assert status == "warn"
+
+
+def test_update_is_success_time():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    ring_len = len(ring.content)
+    ring.update(sig_mock_time)
+    assert len(ring.content) == ring_len + 1
+
+
+def test_ring_is_full_time():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock_time)
+    assert len(ring.content) == data_len / package_len
+
+
+def test_ring_get_sig_time():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock_time)
+    _ = ring.get_sig()
+    assert len(ring.content) == 0
+
+
+def test_enough_ring_get_data_status_is_ok_time():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock_time)
+    data_get = ring.get_sig()
+    status = data_get["status"]
+    data = data_get["data"]
+    my_time_stamp = data_get["timestamp"]
+    assert data.get_data().shape == (len(chan_labels), ring.data_len * fs)
+    assert status == "ok"
+    assert my_time_stamp[0] == 2023
+
+
+def test_not_enough_ring_get_data_status_is_warn_time():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    ring.update(sig_mock_time)
+    data_get = ring.get_sig()
+    status = data_get["status"]
+    data = data_get["data"]
+    my_time_stamp = data_get["timestamp"]
+    assert data is None
+    assert status == "warn"
+    assert my_time_stamp is None
+
+
+def test_get_sig_with_clear():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                            ParserNewsetWithTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock_time)
+    ring.get_sig(clear=True)
+    assert len(ring.content) == 0
+
+
+def test_get_sig_without_clear():
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                            ParserNewsetWithTime())
+    for _ in range(0, int(data_len / package_len)):
+        ring.update(sig_mock_time)
+    ring.get_sig(clear=False)
+    assert len(ring.content) == len(ring.content)
+
+
+def test_not_enough_ring_get_sig_without_clear():
+
+    ring = CircularBuffer(data_len, package_len, chan_labels, chan_types, fs,
+                          ParserNewsetWithTime())
+    ring.update(sig_mock_time)
+    ring.get_sig(clear=False)
+    assert len(ring.content) == 1

+ 33 - 0
backend/tests/core/sig_chain/test_sig_reader.py

@@ -0,0 +1,33 @@
+"""单元测试 sig_reader"""
+import collections
+import os
+
+from core.sig_chain.sig_reader import Reader
+
+TEST_DATA_PATH = "tests/data/"
+BDF_FILE_PATH = os.path.join(TEST_DATA_PATH, "5_3_right_hand.bdf")
+
+
+def test_read():
+    ch_names = [
+        "Fz", "Fp1", "F3", "F7", "C3", "T3", "T5", "P3", "O1", "Cz", "Oz", "Pz",
+        "O2", "P4", "T6", "T4", "C4", "F8", "F4", "Fp2"
+    ]
+    reader = Reader()
+    raw = reader.read(BDF_FILE_PATH, tuple(ch_names))
+    assert (20, 386000) == raw.get_data().shape
+
+
+def test_fix_annotation():
+    ch_names = [
+        "Fz", "Fp1", "F3", "F7", "C3", "T3", "T5", "P3", "O1", "Cz", "Oz", "Pz",
+        "O2", "P4", "T6", "T4", "C4", "F8", "F4", "Fp2"
+    ]
+    reader = Reader()
+    raw = reader.read(BDF_FILE_PATH, tuple(ch_names))
+    reader.fix_annotation(raw)
+
+    ret = collections.Counter(raw.annotations.description)
+    assert 1 == ret["initialRest"]
+    assert 15 == ret["mi"]
+    assert 15 == ret["rest"]

+ 108 - 0
backend/tests/core/sig_chain/test_sig_save.py

@@ -0,0 +1,108 @@
+"""单元测试 sig_save"""
+import os
+
+import numpy as np
+
+from core.sig_chain.sig_save import SigSave
+from schemas.subjects import SubjectCreate
+
+channel_labels = [
+    'T6', 'P4', 'Pz', 'M2', 'F8', 'F4', 'Fp1', 'Cz', 'M1', 'F7', 'F3', 'C3',
+    'T3', 'A1', 'Oz', 'O1', 'O2', 'Fz', 'C4', 'T4', 'Fp2', 'A2', 'T5', 'P3'
+]
+test_data_path = './tests/core/sig_chain/test_data/'
+
+filename = 'testfilename.bdf'
+
+TASK_PER_RUN = 1
+
+
+def setup_module():
+    if not os.path.exists(test_data_path):
+        os.makedirs(test_data_path)
+
+
+def teardown_module():
+    os.removedirs(test_data_path)
+
+
+def create_subject():
+    return SubjectCreate(name='nobody',
+                         id_card='12345',
+                         gender='男',
+                         birthday='1988-01-01',
+                         rehabilitation_parts=['左手'])
+
+
+def test_subject_set_edf_header_success():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    assert saver.is_ready is True
+
+
+def test_close_edf_file():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    saver.close_edf_file()
+    assert saver.is_ready is False
+
+
+def test_save_raw_data_once():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    data = np.ones((len(channel_labels), 1000))
+    saver.save_raw_data(data)
+    file_path = test_data_path + filename
+    assert os.path.exists(file_path)
+    assert os.path.getsize(file_path) > 0
+    saver.close_edf_file()
+    os.remove(file_path)
+
+
+def test_save_raw_data_10_times():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    data = np.ones((len(channel_labels), 1000))
+    for _ in range(10):
+        saver.save_raw_data(data)
+    file_path = test_data_path + filename
+    assert os.path.exists(file_path)
+    assert os.path.getsize(file_path) > 0
+    saver.close_edf_file()
+    os.remove(file_path)
+
+
+def test_save_raw_data_without_set_header():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    data = np.ones((len(channel_labels), 1000))
+    saver.save_raw_data(data)
+    file_path = test_data_path + filename
+    assert os.path.exists(file_path) is False
+
+
+def test_edf_data_mark():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    data = np.ones((len(channel_labels), 1000))
+    saver.save_raw_data(data, 500)
+    file_path = test_data_path + filename
+    saver.edf_data_mark(550, 'OK')
+    saver.close_edf_file()
+    os.remove(file_path)
+
+
+def test_edf_data_mark_timestamp_none():
+    saver = SigSave(channel_labels, 1000, 375000, -375000)
+    subject = create_subject()
+    saver.set_edf_header(subject, filename, TASK_PER_RUN, test_data_path)
+    data = np.ones((len(channel_labels), 1000))
+    saver.save_raw_data(data)
+    file_path = test_data_path + filename
+    saver.edf_data_mark(0.5, 'OK')
+    saver.close_edf_file()
+    os.remove(file_path)

+ 264 - 0
backend/tests/core/test_utils.py

@@ -0,0 +1,264 @@
+"""Test for video analyser """
+import os
+import time
+from unittest.mock import patch
+from unittest.mock import MagicMock
+
+import cv2
+import numpy as np
+
+from core.utils import VideoAnalyser
+
+
+TEST_DATA_PATH = 'tests/data/'
+INPUT_VIDEO_PATH = os.path.join(TEST_DATA_PATH, 'normal_side.mp4')
+OUTPUT_VIDEO_PATH = os.path.join(TEST_DATA_PATH, 'test_base.mp4')
+
+
+def setup_module():
+    if not os.path.exists(TEST_DATA_PATH):
+        os.makedirs(TEST_DATA_PATH)
+
+
+def teardown_function():
+    if os.path.exists(OUTPUT_VIDEO_PATH):
+        os.remove(OUTPUT_VIDEO_PATH)
+
+
+def gen_fake_image():
+    return np.zeros((640, 320, 3), dtype=np.uint8)
+
+
+class TestVideoAnalyser:
+    def test_init_without_input_video_is_camera(self):
+        analyser = VideoAnalyser(input_video=None)
+
+        assert analyser.is_camera
+
+    def test_init_with_input_video_is_not_camera(self):
+        mock_video_capture = MagicMock()
+        mock_video_capture.release = MagicMock()
+        with patch('cv2.VideoCapture', mock_video_capture):
+            analyser = VideoAnalyser(input_video=INPUT_VIDEO_PATH)
+
+        assert not analyser.is_camera
+
+    def test_close_with_opencv_release_resource(self):
+        analyser = VideoAnalyser()
+        analyser.set_output_video(output_video='output.mp4', save_with_av=False)
+
+        analyser.close()
+
+        assert not analyser.cap.isOpened()
+        assert not analyser.out_stream
+
+    def test_close_with_av_release_resource(self):
+        mock_release_container = MagicMock()
+        analyser = VideoAnalyser()
+        analyser.set_output_video(output_video='output.mp4', save_with_av=True)
+        analyser.release_container = mock_release_container
+
+        analyser.close()
+
+        assert not analyser.cap.isOpened()
+        assert not analyser.container
+        assert mock_release_container.called
+
+    def test_set_output_video_with_camera_and_opencv_success(self):
+        analyser = VideoAnalyser(camera_id=0)
+        analyser.open_camera()
+        analyser.set_output_video(output_video='output.mp4', save_with_av=False)
+
+        assert analyser.out_stream
+
+    def test_set_output_video_with_camera_and_av_success(self):
+        analyser = VideoAnalyser(camera_id=0)
+        analyser.open_camera()
+        analyser.set_output_video(output_video='output.mp4', save_with_av=True)
+
+        assert analyser.stream
+        assert analyser.container
+
+    def test_set_output_video_with_video_and_opencv_success(self):
+        analyser = VideoAnalyser(input_video=INPUT_VIDEO_PATH)
+        analyser.set_output_video(output_video='output.mp4', save_with_av=False)
+
+        assert analyser.out_stream
+
+    def test_set_output_video_with_video_and_av_success(self):
+        analyser = VideoAnalyser()
+        analyser.set_output_video(output_video='output.mp4', save_with_av=True)
+
+        assert analyser.stream
+        assert analyser.container
+
+    def test_is_ok_before_cap_open_return_false(self):
+        with patch('cv2.VideoCapture') as mock_cap:
+            mock_cap_instance = mock_cap.return_value
+            mock_cap_instance.isOpened.return_value = False
+            analyser = VideoAnalyser()
+
+            assert not analyser.is_ok()
+
+    def test_is_ok_after_cap_open_return_true(self):
+        with patch('cv2.VideoCapture') as mock_cap:
+            mock_cap_instance = mock_cap.return_value
+            mock_cap_instance.isOpened.return_value = True
+            analyser = VideoAnalyser()
+
+            assert analyser.is_ok()
+
+    def test_process_with_save_when_read_success_save_video(self):
+        with patch('cv2.VideoCapture') as mock_cap:
+            mock_cap_instance = mock_cap.return_value
+            mock_cap_instance.read.return_value = (True, gen_fake_image())
+            mock_save_video = MagicMock()
+
+            analyser = VideoAnalyser()
+            analyser.save_video = mock_save_video
+            analyser.process(save=True)
+
+            assert mock_save_video.called
+
+    def test_process_with_save_when_read_failed_not_save_video(self):
+        with patch('cv2.VideoCapture') as mock_cap:
+            mock_cap_instance = mock_cap.return_value
+            mock_cap_instance.read.return_value = (False, None)
+            mock_save_video = MagicMock()
+
+            analyser = VideoAnalyser()
+            analyser.save_video = mock_save_video
+            analyser.process(save=True)
+
+            assert not mock_save_video.called
+
+    def test_process_without_save_when_read_success_not_save_video(self):
+        with patch('cv2.VideoCapture') as mock_cap:
+            mock_cap_instance = mock_cap.return_value
+            mock_cap_instance.read.return_value = (False, None)
+            mock_save_video = MagicMock()
+
+            analyser = VideoAnalyser()
+            analyser.save_video = mock_save_video
+            analyser.process(save=False)
+
+            assert not mock_save_video.called
+
+    def test_save_video_with_av_av_function_called(self):
+        mock_save_video_with_av = MagicMock()
+
+        analyser = VideoAnalyser()
+        analyser.save_video_with_av = mock_save_video_with_av
+        analyser.save_with_av = True
+        analyser.save_video(gen_fake_image(), time.time())
+
+        assert mock_save_video_with_av.called
+
+    def test_save_video_without_av_opencv_function_called(self):
+        mock_save_video_with_opencv = MagicMock()
+
+        analyser = VideoAnalyser()
+        analyser.save_video_with_opencv = mock_save_video_with_opencv
+        analyser.save_with_av = False
+        analyser.save_video(gen_fake_image(), None)
+
+        assert mock_save_video_with_opencv.called
+
+    def test_save_video_with_opencv_before_set_output_video_pass(self):
+        analyser = VideoAnalyser()
+        analyser.save_video_with_opencv(gen_fake_image())
+
+    def test_save_video_with_opencv_with_out_stream_log_error(self):
+        mock_out_stream = MagicMock()
+        mock_out_stream.isOpened.return_value = False
+        analyser = VideoAnalyser()
+        analyser.out_stream = mock_out_stream
+
+        mock_logging_error = MagicMock()
+        with patch('core.utils.logger.error', mock_logging_error):
+            analyser.save_video_with_opencv(gen_fake_image())
+            assert mock_logging_error.called
+
+    def test_save_video_with_av_before_set_output_video_pass(self):
+        analyser = VideoAnalyser()
+        analyser.save_video_with_av(gen_fake_image(), time.time())
+
+    def test_save_video_with_av_with_antecedent_frame_skipped(self):
+        analyser = VideoAnalyser()
+        analyser.container = MagicMock()
+        analyser.stream = MagicMock()
+        mock_encode = MagicMock()
+        analyser.stream.encode = mock_encode
+
+        t_start = time.time()
+        analyser.t_start_save_video = t_start + 12
+        analyser.save_video_with_av(gen_fake_image(), t_start)
+
+        assert not mock_encode.called
+
+    def test_save_video_with_av_with_valid_frame_success(self):
+        analyser = VideoAnalyser()
+        # analyser = VideoAnalyser(input_video=INPUT_VIDEO_PATH)
+        # analyser.set_output_video(output_video='output.mp4', save_with_av=True)
+        analyser.container = MagicMock()
+        analyser.container.mux = MagicMock()
+        analyser.stream = MagicMock()
+        mock_encode = MagicMock()
+        analyser.stream.encode = mock_encode
+
+        t_start = time.time()
+        analyser.t_start_save_video = t_start - 1
+        analyser.save_video_with_av(gen_fake_image(), t_start)
+
+        assert mock_encode.called
+
+    def test_release_container_without_save_not_finish_with_a_blank_frame(self):
+        mock_av_finish_with_a_blank_frame = MagicMock()
+        analyser = VideoAnalyser()
+        analyser.av_finish_with_a_blank_frame = \
+            mock_av_finish_with_a_blank_frame
+
+        analyser.set_output_video(output_video='output.mp4', save_with_av=True)
+        analyser.release_container()
+
+        assert not mock_av_finish_with_a_blank_frame.called
+
+    def test_release_container_reset_time_start_save_video(self):
+        mock_av_finish_with_a_blank_frame = MagicMock()
+        analyser = VideoAnalyser()
+        analyser.av_finish_with_a_blank_frame = \
+            mock_av_finish_with_a_blank_frame
+        analyser.t_start_save_video = time.time()
+
+        analyser.release_container()
+
+        assert not analyser.t_start_save_video
+
+    def test_release_container_reset_previous_pts(self):
+        mock_av_finish_with_a_blank_frame = MagicMock()
+        analyser = VideoAnalyser()
+        analyser.av_finish_with_a_blank_frame = \
+            mock_av_finish_with_a_blank_frame
+        analyser.previous_pts = 134
+
+        analyser.release_container()
+
+        assert 0 == analyser.previous_pts
+
+
+def test_main():
+    analyser = VideoAnalyser()
+    # analyser.set_output_video(output_video=OUTPUT_VIDEO_PATH, save_with_av=True)
+    analyser.set_output_video(output_video=OUTPUT_VIDEO_PATH)
+    count = 0
+    while analyser.is_ok():
+        count += 1
+        if count == 196:
+            break
+
+        _, image = analyser.process()
+        cv2.imshow('base', image)
+        if cv2.waitKey(1) & 0xFF == ord('q'): #press q to quit
+            break
+
+    cv2.destroyAllWindows()

BIN
backend/tests/data/5_3_right_hand.bdf


ファイルの差分が大きいため隠しています
+ 0 - 0
backend/tests/data/eeg_raw_data.bdf


BIN
backend/tests/data/neo_eeg_raw_data.bdf


BIN
backend/tests/data/normal_side.mp4


+ 0 - 0
backend/tests/utils/__init__.py


+ 36 - 0
backend/tests/utils/core.py

@@ -0,0 +1,36 @@
+"""
+Author: linxiaohong linxiaohong@neuracle.cn
+Date: 2023-07-17 14:14:20
+LastEditors: linxiaohong linxiaohong@neuracle.cn
+LastEditTime: 2023-07-19 14:02:01
+FilePath: Albatross/backend/tests/utils/core.py
+Description: tests/core 中的测试共用的工具函数
+
+Copyright (c) 2023 by Neuracle, All Rights Reserved.
+"""
+import mne
+
+
+def get_epochs(raw, picks, event_name=None, tmin=0, tmax=1):
+    events, event_id = mne.events_from_annotations(raw)
+    if event_name is None:
+        event_id_pick = event_id
+    else:
+        event_id_pick = {event_name: event_id[event_name]}
+    epochs = mne.Epochs(raw,
+                        events,
+                        event_id_pick,
+                        tmin,
+                        tmax,
+                        picks=picks,
+                        baseline=None,
+                        preload=True)
+    return epochs
+
+
+def crop_by_annotation(raw, annot):
+    onset = annot["onset"] - raw.first_time
+    if -raw.info["sfreq"] / 2 < onset < 0:
+        onset = 0
+    raw_crop = raw.copy().crop(onset, onset + annot["duration"])
+    return raw_crop

+ 57 - 0
backend/tests/utils/subject.py

@@ -0,0 +1,57 @@
+"""testing subjects utils"""
+import random
+
+from sqlalchemy.orm import Session
+
+from db.repository import subjects as db_rep_sub
+from schemas.subjects import SubjectCreate
+from utils.utils import fake
+
+
+def generate_subject_fake_data():
+    return {
+        "name":
+            fake.name(),
+        "id_card":
+            None,
+        "birthday":
+            str(fake.date_between_dates(date_start="-100y", date_end="-5y")),
+        "gender":
+            fake.subject_gender(),
+        "rehabilitation_parts":
+            fake.rehabilitation_parts()
+    }
+
+
+def create_test_subject2db(db: Session,
+                       name=fake.name(),
+                       id_card=None,
+                       gender=fake.subject_gender(),
+                       birthday=fake.date_between_dates(date_start="-100y",
+                                                        date_end="-5y"),
+                       rehabilitation_parts=fake.rehabilitation_parts(),
+                       create_time=None) -> SubjectCreate:
+    if create_time is None:
+        subject = SubjectCreate(name=name,
+                                id_card=id_card,
+                                gender=gender,
+                                birthday=birthday,
+                                rehabilitation_parts=rehabilitation_parts)
+    else:
+        subject = SubjectCreate(name=name,
+                                id_card=id_card,
+                                gender=gender,
+                                birthday=birthday,
+                                rehabilitation_parts=rehabilitation_parts,
+                                create_time=create_time)
+    subject = db_rep_sub.create_subject(subject, db)
+    return subject
+
+
+def get_all_subject(db: Session):
+    return db_rep_sub.list_subjects(db=db)
+
+
+def get_random_existing_subject(db: Session):
+    subjects = get_all_subject(db)
+    return random.choice(subjects)

+ 38 - 0
backend/tests/utils/train.py

@@ -0,0 +1,38 @@
+"""testing subjects utils"""
+from sqlalchemy.orm import Session
+
+from db.repository import trains as db_rep_train
+from schemas.trains import TrainCreate
+from utils.utils import fake
+from utils.utils import get_random_position
+
+
+def generate_fake_train_data():
+    return {
+        "position": "左手",
+        "rank": fake.train_rank(),
+        "trial_num": fake.random_digit_not_null(),
+        "start_time": "2022-11-03 00:00",
+        "end_time": "2022-11-04 00:00"
+    }
+
+
+def create_test_train2db(db: Session,
+                      subject,
+                      position=None,
+                      rank=fake.train_rank(),
+                      trial_num=fake.random_digit_not_null(),
+                      start_time="2022-11-03 00:00",
+                      end_time="2022-11-04 00:00",
+                      device_parm=None) -> TrainCreate:
+    if position is None:
+        position = get_random_position(subject)
+    train = TrainCreate(position=position,
+                        rank=rank,
+                        trial_num=trial_num,
+                        start_time=start_time,
+                        end_time=end_time,
+                        device_parm=device_parm,
+                        owner_id=subject.id)
+    train = db_rep_train.create_train(train, db)
+    return train

+ 72 - 0
backend/tests/utils/utils.py

@@ -0,0 +1,72 @@
+"""provide fake data obj"""
+from datetime import datetime, timedelta
+import itertools
+import random
+
+from faker import Faker
+from faker.providers import DynamicProvider
+from sqlalchemy.orm import Session
+
+from db.models.subjects import Subject
+from db.models.trains import Train
+from db.models.hand_peripherals import HandPeripheral
+from db.models.daily_stats import DailyStats
+from db.repository import subjects as db_rep_sub
+from schemas.subjects import SubjectCreate
+
+
+class FakerManager:
+    """init fake obj"""
+
+    def __init__(self, lang="zh-cn"):
+        self.fake = Faker(lang)
+        self.load_provider()
+
+    def load_provider(self):
+        self.fake.add_provider(self.get_gender_provider())
+        self.fake.add_provider(self.get_parts_provider())
+        self.fake.add_provider(self.get_train_rank_provider())
+
+    @staticmethod
+    def get_gender_provider():
+        return DynamicProvider(provider_name="subject_gender",
+                               elements=["男", "女"])
+
+    @staticmethod
+    def get_parts_provider():
+        parts_list = []
+        for num in range(1, 5):
+            parts_list.extend(
+                list(itertools.combinations(["左手", "右手", "左腿", "右腿"], num)))
+        parts_provider = DynamicProvider(provider_name="rehabilitation_parts",
+                                         elements=parts_list)
+        return parts_provider
+
+    @staticmethod
+    def get_train_rank_provider():
+        return DynamicProvider(provider_name="train_rank",
+                               elements=["简单", "中等", "困难"])
+
+
+
+def get_random_position(subject):
+    return random.choice(subject.rehabilitation_parts)
+
+
+def generate_delay_datetime(delay_years: int):
+    today = datetime.today()
+    delay_time = timedelta(days=365*delay_years)
+    delay_datetime = today + delay_time
+    return delay_datetime.strftime("%Y-%m-%d")
+
+
+def clear_db_table(db: Session):
+    db.query(Subject).delete()
+    db.query(Train).delete()
+    db.query(HandPeripheral).delete()
+    db.query(DailyStats).delete()
+    db.commit()
+
+
+fake = FakerManager().fake
+

+ 1300 - 0
backend/train_1.py

@@ -0,0 +1,1300 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+This experiment was created using PsychoPy3 Experiment Builder (v2023.2.3),
+    on 十一月 08, 2023, at 14:34
+If you publish work using this script the most relevant publication is:
+
+    Peirce J, Gray JR, Simpson S, MacAskill M, Höchenberger R, Sogo H, Kastman E, Lindeløv JK. (2019) 
+        PsychoPy2: Experiments in behavior made easy Behav Res 51: 195. 
+        https://doi.org/10.3758/s13428-018-01193-y
+
+"""
+
+# --- Import packages ---
+from psychopy import locale_setup
+from psychopy import prefs
+from psychopy import plugins
+plugins.activatePlugins()
+prefs.hardware['audioLib'] = 'ptb'
+prefs.hardware['audioLatencyMode'] = '3'
+from psychopy import sound, gui, visual, core, data, event, logging, clock, colors, layout
+from psychopy.tools import environmenttools
+from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
+                                STOPPED, FINISHED, PRESSED, RELEASED, FOREVER, priority)
+
+import numpy as np  # whole numpy lib is available, prepend 'np.'
+from numpy import (sin, cos, tan, log, log10, pi, average,
+                   sqrt, std, deg2rad, rad2deg, linspace, asarray)
+from numpy.random import random, randint, normal, shuffle, choice as randchoice
+import os  # handy system and path functions
+import sys  # to get file system encoding
+
+import psychopy.iohub as io
+from psychopy.hardware import keyboard
+
+# Run 'Before Experiment' code from exp_prepare_code
+import sqlite3
+from time import sleep
+
+import streamlit as st
+from db.models import train
+from core.sig_chain.sig_receive import Receiver
+from core.sig_chain.device.connector_interface import Device
+from settings.config import settings
+
+# get train record
+con = sqlite3.connect("./sql_app.db")
+cur = con.cursor()
+sql_param = "SELECT * FROM train ORDER BY start_time DESC"
+res = cur.execute(sql_param)
+exp_train = res.fetchone()
+cur.close()
+
+# connect device
+receiver = Receiver()
+config_info = settings.CONFIG_INFO
+receiver.select_connector(Device.NEO, 0.04, config_info)
+success = receiver.setup_connector()
+print(success)
+# begin to receive data from device.
+sleep(1)
+receiver.start_receive_wave()
+
+
+# --- Setup global variables (available in all functions) ---
+# Ensure that relative paths start from the same directory as this script
+_thisDir = os.path.dirname(os.path.abspath(__file__))
+# Store info about the experiment session
+psychopyVersion = '2023.2.3'
+expName = 'train'  # from the Builder filename that created this script
+expInfo = {
+    'participant': f"{randint(0, 999999):06.0f}",
+    'session': '001',
+    'date': data.getDateStr(),  # add a simple timestamp
+    'expName': expName,
+    'psychopyVersion': psychopyVersion,
+}
+
+
+def showExpInfoDlg(expInfo):
+    """
+    Show participant info dialog.
+    Parameters
+    ==========
+    expInfo : dict
+        Information about this experiment, created by the `setupExpInfo` function.
+    
+    Returns
+    ==========
+    dict
+        Information about this experiment.
+    """
+    # temporarily remove keys which the dialog doesn't need to show
+    poppedKeys = {
+        'date': expInfo.pop('date', data.getDateStr()),
+        'expName': expInfo.pop('expName', expName),
+        'psychopyVersion': expInfo.pop('psychopyVersion', psychopyVersion),
+    }
+    # show participant info dialog
+    dlg = gui.DlgFromDict(dictionary=expInfo, sortKeys=False, title=expName)
+    if dlg.OK == False:
+        core.quit()  # user pressed cancel
+    # restore hidden keys
+    expInfo.update(poppedKeys)
+    # return expInfo
+    return expInfo
+
+
+def setupData(expInfo, dataDir=None):
+    """
+    Make an ExperimentHandler to handle trials and saving.
+    
+    Parameters
+    ==========
+    expInfo : dict
+        Information about this experiment, created by the `setupExpInfo` function.
+    dataDir : Path, str or None
+        Folder to save the data to, leave as None to create a folder in the current directory.    
+    Returns
+    ==========
+    psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    """
+    
+    # data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
+    if dataDir is None:
+        dataDir = _thisDir
+    filename = u'data/%s_%s_%s' % (expInfo['participant'], expName, expInfo['date'])
+    # make sure filename is relative to dataDir
+    if os.path.isabs(filename):
+        dataDir = os.path.commonprefix([dataDir, filename])
+        filename = os.path.relpath(filename, dataDir)
+    
+    # an ExperimentHandler isn't essential but helps with data saving
+    thisExp = data.ExperimentHandler(
+        name=expName, version='',
+        extraInfo=expInfo, runtimeInfo=None,
+        originPath='C:\\Users\\zhengyan\\Desktop\\back\\train\\train.py',
+        savePickle=True, saveWideText=True,
+        dataFileName=dataDir + os.sep + filename, sortColumns='time'
+    )
+    thisExp.setPriority('thisRow.t', priority.CRITICAL)
+    thisExp.setPriority('expName', priority.LOW)
+    # return experiment handler
+    return thisExp
+
+
+def setupLogging(filename):
+    """
+    Setup a log file and tell it what level to log at.
+    
+    Parameters
+    ==========
+    filename : str or pathlib.Path
+        Filename to save log file and data files as, doesn't need an extension.
+    
+    Returns
+    ==========
+    psychopy.logging.LogFile
+        Text stream to receive inputs from the logging system.
+    """
+    # this outputs to the screen, not a file
+    logging.console.setLevel(logging.EXP)
+    # save a log file for detail verbose info
+    logFile = logging.LogFile(filename+'.log', level=logging.EXP)
+    
+    return logFile
+
+
+def setupWindow(expInfo=None, win=None):
+    """
+    Setup the Window
+    
+    Parameters
+    ==========
+    expInfo : dict
+        Information about this experiment, created by the `setupExpInfo` function.
+    win : psychopy.visual.Window
+        Window to setup - leave as None to create a new window.
+    
+    Returns
+    ==========
+    psychopy.visual.Window
+        Window in which to run this experiment.
+    """
+    if win is None:
+        # if not given a window to setup, make one
+        win = visual.Window(
+            size=(1024, 768), fullscr=True, screen=0,
+            winType='pyglet', allowStencil=False,
+            monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
+            backgroundImage='', backgroundFit='none',
+            blendMode='avg', useFBO=True,
+            units='height'
+        )
+        if expInfo is not None:
+            # store frame rate of monitor if we can measure it
+            expInfo['frameRate'] = win.getActualFrameRate()
+    else:
+        # if we have a window, just set the attributes which are safe to set
+        win.color = [0,0,0]
+        win.colorSpace = 'rgb'
+        win.backgroundImage = ''
+        win.backgroundFit = 'none'
+        win.units = 'height'
+    win.mouseVisible = False
+    win.hideMessage()
+    return win
+
+
+def setupInputs(expInfo, thisExp, win):
+    """
+    Setup whatever inputs are available (mouse, keyboard, eyetracker, etc.)
+    
+    Parameters
+    ==========
+    expInfo : dict
+        Information about this experiment, created by the `setupExpInfo` function.
+    thisExp : psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    win : psychopy.visual.Window
+        Window in which to run this experiment.
+    Returns
+    ==========
+    dict
+        Dictionary of input devices by name.
+    """
+    # --- Setup input devices ---
+    inputs = {}
+    ioConfig = {}
+    
+    # Setup iohub keyboard
+    ioConfig['Keyboard'] = dict(use_keymap='psychopy')
+    
+    ioSession = '1'
+    if 'session' in expInfo:
+        ioSession = str(expInfo['session'])
+    ioServer = io.launchHubServer(window=win, **ioConfig)
+    eyetracker = None
+    
+    # create a default keyboard (e.g. to check for escape)
+    defaultKeyboard = keyboard.Keyboard(backend='iohub')
+    # return inputs dict
+    return {
+        'ioServer': ioServer,
+        'defaultKeyboard': defaultKeyboard,
+        'eyetracker': eyetracker,
+    }
+
+def pauseExperiment(thisExp, inputs=None, win=None, timers=[], playbackComponents=[]):
+    """
+    Pause this experiment, preventing the flow from advancing to the next routine until resumed.
+    
+    Parameters
+    ==========
+    thisExp : psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    inputs : dict
+        Dictionary of input devices by name.
+    win : psychopy.visual.Window
+        Window for this experiment.
+    timers : list, tuple
+        List of timers to reset once pausing is finished.
+    playbackComponents : list, tuple
+        List of any components with a `pause` method which need to be paused.
+    """
+    # if we are not paused, do nothing
+    if thisExp.status != PAUSED:
+        return
+    
+    # pause any playback components
+    for comp in playbackComponents:
+        comp.pause()
+    # prevent components from auto-drawing
+    win.stashAutoDraw()
+    # run a while loop while we wait to unpause
+    while thisExp.status == PAUSED:
+        # make sure we have a keyboard
+        if inputs is None:
+            inputs = {
+                'defaultKeyboard': keyboard.Keyboard(backend='ioHub')
+            }
+        # check for quit (typically the Esc key)
+        if inputs['defaultKeyboard'].getKeys(keyList=['escape']):
+            endExperiment(thisExp, win=win, inputs=inputs)
+        # flip the screen
+        win.flip()
+    # if stop was requested while paused, quit
+    if thisExp.status == FINISHED:
+        endExperiment(thisExp, inputs=inputs, win=win)
+    # resume any playback components
+    for comp in playbackComponents:
+        comp.play()
+    # restore auto-drawn components
+    win.retrieveAutoDraw()
+    # reset any timers
+    for timer in timers:
+        timer.reset()
+
+
+def run(expInfo, thisExp, win, inputs, globalClock=None, thisSession=None):
+    """
+    Run the experiment flow.
+    
+    Parameters
+    ==========
+    expInfo : dict
+        Information about this experiment, created by the `setupExpInfo` function.
+    thisExp : psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    psychopy.visual.Window
+        Window in which to run this experiment.
+    inputs : dict
+        Dictionary of input devices by name.
+    globalClock : psychopy.core.clock.Clock or None
+        Clock to get global time from - supply None to make a new one.
+    thisSession : psychopy.session.Session or None
+        Handle of the Session object this experiment is being run from, if any.
+    """
+    # mark experiment as started
+    thisExp.status = STARTED
+    # make sure variables created by exec are available globally
+    exec = environmenttools.setExecEnvironment(globals())
+    # get device handles from dict of input devices
+    ioServer = inputs['ioServer']
+    defaultKeyboard = inputs['defaultKeyboard']
+    eyetracker = inputs['eyetracker']
+    # make sure we're running in the directory for this experiment
+    os.chdir(_thisDir)
+    # get filename from ExperimentHandler for convenience
+    filename = thisExp.dataFileName
+    frameTolerance = 0.001  # how close to onset before 'same' frame
+    endExpNow = False  # flag for 'escape' or other condition => quit the exp
+    # get frame duration from frame rate in expInfo
+    if 'frameRate' in expInfo and expInfo['frameRate'] is not None:
+        frameDur = 1.0 / round(expInfo['frameRate'])
+    else:
+        frameDur = 1.0 / 60.0  # could not measure, so guess
+    
+    # Start Code - component code to be run after the window creation
+    
+    # --- Initialize components for Routine "exp_prepare" ---
+    prepare = visual.TextStim(win=win, name='prepare',
+        text='实验准备中...',
+        font='Open Sans',
+        pos=(0, 0), height=0.05, wrapWidth=None, ori=0.0, 
+        color='white', colorSpace='rgb', opacity=None, 
+        languageStyle='LTR',
+        depth=0.0);
+    
+    # --- Initialize components for Routine "before_mi" ---
+    train_position = visual.TextStim(win=win, name='train_position',
+        text="训练部位:" + exp_train[0],
+        font='Open Sans',
+        pos=(0, 0), height=0.05, wrapWidth=None, ori=0.0, 
+        color='white', colorSpace='rgb', opacity=None, 
+        languageStyle='LTR',
+        depth=0.0);
+    instruction = visual.TextStim(win=win, name='instruction',
+        text='静息态采集\n请保持放松,注视十字准星',
+        font='Open Sans',
+        pos=(0, 0), height=0.05, wrapWidth=None, ori=0.0, 
+        color='white', colorSpace='rgb', opacity=None, 
+        languageStyle='LTR',
+        depth=-1.0);
+    img_reststate = visual.ImageStim(
+        win=win,
+        name='img_reststate', 
+        image='C:/Users/zhengyan/myWork/py_work/Kraken/Albatross/backend/static/images/reststate.png', mask=None, anchor='center',
+        ori=0.0, pos=(0, 0), size=(0.5, 0.5),
+        color=[1,1,1], colorSpace='rgb', opacity=None,
+        flipHoriz=False, flipVert=False,
+        texRes=128.0, interpolate=True, depth=-2.0)
+    
+    # --- Initialize components for Routine "mi_prepare" ---
+    img_prepare = visual.ImageStim(
+        win=win,
+        name='img_prepare', 
+        image='C:/Users/zhengyan/myWork/py_work/Kraken/Albatross/backend/static/images/reststate.png', mask=None, anchor='center',
+        ori=0.0, pos=(0, 0), size=(0.5, 0.5),
+        color=[1,1,1], colorSpace='rgb', opacity=None,
+        flipHoriz=False, flipVert=False,
+        texRes=128.0, interpolate=True, depth=0.0)
+    
+    # --- Initialize components for Routine "mi_begin" ---
+    img_right = visual.ImageStim(
+        win=win,
+        name='img_right', 
+        image='C:/Users/zhengyan/myWork/py_work/Kraken/Albatross/backend/static/images/right.png', mask=None, anchor='center',
+        ori=0.0, pos=(0, 0), size=(0.5, 0.5),
+        color=[1,1,1], colorSpace='rgb', opacity=None,
+        flipHoriz=False, flipVert=False,
+        texRes=128.0, interpolate=True, depth=0.0)
+    
+    # --- Initialize components for Routine "mi_feedback" ---
+    feedback = visual.TextStim(win=win, name='feedback',
+        text='反馈',
+        font='Open Sans',
+        pos=(0, 0), height=0.05, wrapWidth=None, ori=0.0, 
+        color='white', colorSpace='rgb', opacity=None, 
+        languageStyle='LTR',
+        depth=0.0);
+    
+    # --- Initialize components for Routine "mi_rest" ---
+    img_rest = visual.ImageStim(
+        win=win,
+        name='img_rest', 
+        image='C:/Users/zhengyan/myWork/py_work/Kraken/Albatross/backend/static/images/rest.png', mask=None, anchor='center',
+        ori=0.0, pos=(0, 0), size=(0.5, 0.5),
+        color=[1,1,1], colorSpace='rgb', opacity=None,
+        flipHoriz=False, flipVert=False,
+        texRes=128.0, interpolate=True, depth=0.0)
+    
+    # --- Initialize components for Routine "end" ---
+    mi_end = visual.TextStim(win=win, name='mi_end',
+        text='结束实验',
+        font='Open Sans',
+        pos=(0, 0), height=0.05, wrapWidth=None, ori=0.0, 
+        color='white', colorSpace='rgb', opacity=None, 
+        languageStyle='LTR',
+        depth=0.0);
+    
+    # create some handy timers
+    if globalClock is None:
+        globalClock = core.Clock()  # to track the time since experiment started
+    if ioServer is not None:
+        ioServer.syncClock(globalClock)
+    logging.setDefaultClock(globalClock)
+    routineTimer = core.Clock()  # to track time remaining of each (possibly non-slip) routine
+    win.flip()  # flip window to reset last flip timer
+    # store the exact time the global clock started
+    expInfo['expStart'] = data.getDateStr(format='%Y-%m-%d %Hh%M.%S.%f %z', fractionalSecondDigits=6)
+    
+    # --- Prepare to start Routine "exp_prepare" ---
+    continueRoutine = True
+    # update component parameters for each repeat
+    thisExp.addData('exp_prepare.started', globalClock.getTime())
+    # keep track of which components have finished
+    exp_prepareComponents = [prepare]
+    for thisComponent in exp_prepareComponents:
+        thisComponent.tStart = None
+        thisComponent.tStop = None
+        thisComponent.tStartRefresh = None
+        thisComponent.tStopRefresh = None
+        if hasattr(thisComponent, 'status'):
+            thisComponent.status = NOT_STARTED
+    # reset timers
+    t = 0
+    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+    frameN = -1
+    
+    # --- Run Routine "exp_prepare" ---
+    routineForceEnded = not continueRoutine
+    while continueRoutine and routineTimer.getTime() < 3.0:
+        # get current time
+        t = routineTimer.getTime()
+        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+        # update/draw components on each frame
+        
+        # *prepare* updates
+        
+        # if prepare is starting this frame...
+        if prepare.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+            # keep track of start time/frame for later
+            prepare.frameNStart = frameN  # exact frame index
+            prepare.tStart = t  # local t and not account for scr refresh
+            prepare.tStartRefresh = tThisFlipGlobal  # on global time
+            win.timeOnFlip(prepare, 'tStartRefresh')  # time at next scr refresh
+            # add timestamp to datafile
+            thisExp.timestampOnFlip(win, 'prepare.started')
+            # update status
+            prepare.status = STARTED
+            prepare.setAutoDraw(True)
+        
+        # if prepare is active this frame...
+        if prepare.status == STARTED:
+            # update params
+            pass
+        
+        # if prepare is stopping this frame...
+        if prepare.status == STARTED:
+            # is it time to stop? (based on global clock, using actual start)
+            if tThisFlipGlobal > prepare.tStartRefresh + 3-frameTolerance:
+                # keep track of stop time/frame for later
+                prepare.tStop = t  # not accounting for scr refresh
+                prepare.frameNStop = frameN  # exact frame index
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'prepare.stopped')
+                # update status
+                prepare.status = FINISHED
+                prepare.setAutoDraw(False)
+        
+        # check for quit (typically the Esc key)
+        if defaultKeyboard.getKeys(keyList=["escape"]):
+            thisExp.status = FINISHED
+        if thisExp.status == FINISHED or endExpNow:
+            endExperiment(thisExp, inputs=inputs, win=win)
+            return
+        
+        # check if all components have finished
+        if not continueRoutine:  # a component has requested a forced-end of Routine
+            routineForceEnded = True
+            break
+        continueRoutine = False  # will revert to True if at least one component still running
+        for thisComponent in exp_prepareComponents:
+            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                continueRoutine = True
+                break  # at least one component has not yet finished
+        
+        # refresh the screen
+        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+            win.flip()
+    
+    # --- Ending Routine "exp_prepare" ---
+    for thisComponent in exp_prepareComponents:
+        if hasattr(thisComponent, "setAutoDraw"):
+            thisComponent.setAutoDraw(False)
+    thisExp.addData('exp_prepare.stopped', globalClock.getTime())
+    # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+    if routineForceEnded:
+        routineTimer.reset()
+    else:
+        routineTimer.addTime(-3.000000)
+    
+    # --- Prepare to start Routine "before_mi" ---
+    continueRoutine = True
+    # update component parameters for each repeat
+    thisExp.addData('before_mi.started', globalClock.getTime())
+    # keep track of which components have finished
+    before_miComponents = [train_position, instruction, img_reststate]
+    for thisComponent in before_miComponents:
+        thisComponent.tStart = None
+        thisComponent.tStop = None
+        thisComponent.tStartRefresh = None
+        thisComponent.tStopRefresh = None
+        if hasattr(thisComponent, 'status'):
+            thisComponent.status = NOT_STARTED
+    # reset timers
+    t = 0
+    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+    frameN = -1
+    
+    # --- Run Routine "before_mi" ---
+    routineForceEnded = not continueRoutine
+    while continueRoutine and routineTimer.getTime() < 16.5:
+        # get current time
+        t = routineTimer.getTime()
+        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+        # update/draw components on each frame
+        
+        # *train_position* updates
+        
+        # if train_position is starting this frame...
+        if train_position.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+            # keep track of start time/frame for later
+            train_position.frameNStart = frameN  # exact frame index
+            train_position.tStart = t  # local t and not account for scr refresh
+            train_position.tStartRefresh = tThisFlipGlobal  # on global time
+            win.timeOnFlip(train_position, 'tStartRefresh')  # time at next scr refresh
+            # add timestamp to datafile
+            thisExp.timestampOnFlip(win, 'train_position.started')
+            # update status
+            train_position.status = STARTED
+            train_position.setAutoDraw(True)
+        
+        # if train_position is active this frame...
+        if train_position.status == STARTED:
+            # update params
+            pass
+        
+        # if train_position is stopping this frame...
+        if train_position.status == STARTED:
+            # is it time to stop? (based on global clock, using actual start)
+            if tThisFlipGlobal > train_position.tStartRefresh + 2-frameTolerance:
+                # keep track of stop time/frame for later
+                train_position.tStop = t  # not accounting for scr refresh
+                train_position.frameNStop = frameN  # exact frame index
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'train_position.stopped')
+                # update status
+                train_position.status = FINISHED
+                train_position.setAutoDraw(False)
+        
+        # *instruction* updates
+        
+        # if instruction is starting this frame...
+        if instruction.status == NOT_STARTED and tThisFlip >= 3-frameTolerance:
+            # keep track of start time/frame for later
+            instruction.frameNStart = frameN  # exact frame index
+            instruction.tStart = t  # local t and not account for scr refresh
+            instruction.tStartRefresh = tThisFlipGlobal  # on global time
+            win.timeOnFlip(instruction, 'tStartRefresh')  # time at next scr refresh
+            # add timestamp to datafile
+            thisExp.timestampOnFlip(win, 'instruction.started')
+            # update status
+            instruction.status = STARTED
+            instruction.setAutoDraw(True)
+        
+        # if instruction is active this frame...
+        if instruction.status == STARTED:
+            # update params
+            pass
+        
+        # if instruction is stopping this frame...
+        if instruction.status == STARTED:
+            # is it time to stop? (based on global clock, using actual start)
+            if tThisFlipGlobal > instruction.tStartRefresh + 2-frameTolerance:
+                # keep track of stop time/frame for later
+                instruction.tStop = t  # not accounting for scr refresh
+                instruction.frameNStop = frameN  # exact frame index
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'instruction.stopped')
+                # update status
+                instruction.status = FINISHED
+                instruction.setAutoDraw(False)
+        
+        # *img_reststate* updates
+        
+        # if img_reststate is starting this frame...
+        if img_reststate.status == NOT_STARTED and tThisFlip >= 6.5-frameTolerance:
+            # keep track of start time/frame for later
+            img_reststate.frameNStart = frameN  # exact frame index
+            img_reststate.tStart = t  # local t and not account for scr refresh
+            img_reststate.tStartRefresh = tThisFlipGlobal  # on global time
+            win.timeOnFlip(img_reststate, 'tStartRefresh')  # time at next scr refresh
+            # add timestamp to datafile
+            thisExp.timestampOnFlip(win, 'img_reststate.started')
+            # update status
+            img_reststate.status = STARTED
+            img_reststate.setAutoDraw(True)
+        
+        # if img_reststate is active this frame...
+        if img_reststate.status == STARTED:
+            # update params
+            pass
+        
+        # if img_reststate is stopping this frame...
+        if img_reststate.status == STARTED:
+            # is it time to stop? (based on global clock, using actual start)
+            if tThisFlipGlobal > img_reststate.tStartRefresh + 10-frameTolerance:
+                # keep track of stop time/frame for later
+                img_reststate.tStop = t  # not accounting for scr refresh
+                img_reststate.frameNStop = frameN  # exact frame index
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'img_reststate.stopped')
+                # update status
+                img_reststate.status = FINISHED
+                img_reststate.setAutoDraw(False)
+        
+        # check for quit (typically the Esc key)
+        if defaultKeyboard.getKeys(keyList=["escape"]):
+            thisExp.status = FINISHED
+        if thisExp.status == FINISHED or endExpNow:
+            endExperiment(thisExp, inputs=inputs, win=win)
+            return
+        
+        # check if all components have finished
+        if not continueRoutine:  # a component has requested a forced-end of Routine
+            routineForceEnded = True
+            break
+        continueRoutine = False  # will revert to True if at least one component still running
+        for thisComponent in before_miComponents:
+            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                continueRoutine = True
+                break  # at least one component has not yet finished
+        
+        # refresh the screen
+        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+            win.flip()
+    
+    # --- Ending Routine "before_mi" ---
+    for thisComponent in before_miComponents:
+        if hasattr(thisComponent, "setAutoDraw"):
+            thisComponent.setAutoDraw(False)
+    thisExp.addData('before_mi.stopped', globalClock.getTime())
+    # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+    if routineForceEnded:
+        routineTimer.reset()
+    else:
+        routineTimer.addTime(-16.500000)
+    
+    # set up handler to look after randomisation of conditions etc
+    trials = data.TrialHandler(nReps=exp_train[1], method='random', 
+        extraInfo=expInfo, originPath=-1,
+        trialList=[None],
+        seed=None, name='trials')
+    thisExp.addLoop(trials)  # add the loop to the experiment
+    thisTrial = trials.trialList[0]  # so we can initialise stimuli with some values
+    # abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
+    if thisTrial != None:
+        for paramName in thisTrial:
+            globals()[paramName] = thisTrial[paramName]
+    
+    for thisTrial in trials:
+        currentLoop = trials
+        thisExp.timestampOnFlip(win, 'thisRow.t')
+        # pause experiment here if requested
+        if thisExp.status == PAUSED:
+            pauseExperiment(
+                thisExp=thisExp, 
+                inputs=inputs, 
+                win=win, 
+                timers=[routineTimer], 
+                playbackComponents=[]
+        )
+        # abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
+        if thisTrial != None:
+            for paramName in thisTrial:
+                globals()[paramName] = thisTrial[paramName]
+        
+        # --- Prepare to start Routine "mi_prepare" ---
+        continueRoutine = True
+        # update component parameters for each repeat
+        thisExp.addData('mi_prepare.started', globalClock.getTime())
+        # keep track of which components have finished
+        mi_prepareComponents = [img_prepare]
+        for thisComponent in mi_prepareComponents:
+            thisComponent.tStart = None
+            thisComponent.tStop = None
+            thisComponent.tStartRefresh = None
+            thisComponent.tStopRefresh = None
+            if hasattr(thisComponent, 'status'):
+                thisComponent.status = NOT_STARTED
+        # reset timers
+        t = 0
+        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+        frameN = -1
+        
+        # --- Run Routine "mi_prepare" ---
+        routineForceEnded = not continueRoutine
+        while continueRoutine and routineTimer.getTime() < 1.5:
+            # get current time
+            t = routineTimer.getTime()
+            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+            # update/draw components on each frame
+            
+            # *img_prepare* updates
+            
+            # if img_prepare is starting this frame...
+            if img_prepare.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+                # keep track of start time/frame for later
+                img_prepare.frameNStart = frameN  # exact frame index
+                img_prepare.tStart = t  # local t and not account for scr refresh
+                img_prepare.tStartRefresh = tThisFlipGlobal  # on global time
+                win.timeOnFlip(img_prepare, 'tStartRefresh')  # time at next scr refresh
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'img_prepare.started')
+                # update status
+                img_prepare.status = STARTED
+                img_prepare.setAutoDraw(True)
+            
+            # if img_prepare is active this frame...
+            if img_prepare.status == STARTED:
+                # update params
+                pass
+            
+            # if img_prepare is stopping this frame...
+            if img_prepare.status == STARTED:
+                # is it time to stop? (based on global clock, using actual start)
+                if tThisFlipGlobal > img_prepare.tStartRefresh + 1.5-frameTolerance:
+                    # keep track of stop time/frame for later
+                    img_prepare.tStop = t  # not accounting for scr refresh
+                    img_prepare.frameNStop = frameN  # exact frame index
+                    # add timestamp to datafile
+                    thisExp.timestampOnFlip(win, 'img_prepare.stopped')
+                    # update status
+                    img_prepare.status = FINISHED
+                    img_prepare.setAutoDraw(False)
+            
+            # check for quit (typically the Esc key)
+            if defaultKeyboard.getKeys(keyList=["escape"]):
+                thisExp.status = FINISHED
+            if thisExp.status == FINISHED or endExpNow:
+                endExperiment(thisExp, inputs=inputs, win=win)
+                return
+            
+            # check if all components have finished
+            if not continueRoutine:  # a component has requested a forced-end of Routine
+                routineForceEnded = True
+                break
+            continueRoutine = False  # will revert to True if at least one component still running
+            for thisComponent in mi_prepareComponents:
+                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                    continueRoutine = True
+                    break  # at least one component has not yet finished
+            
+            # refresh the screen
+            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+                win.flip()
+        
+        # --- Ending Routine "mi_prepare" ---
+        for thisComponent in mi_prepareComponents:
+            if hasattr(thisComponent, "setAutoDraw"):
+                thisComponent.setAutoDraw(False)
+        thisExp.addData('mi_prepare.stopped', globalClock.getTime())
+        # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+        if routineForceEnded:
+            routineTimer.reset()
+        else:
+            routineTimer.addTime(-1.500000)
+        
+        # --- Prepare to start Routine "mi_begin" ---
+        continueRoutine = True
+        # update component parameters for each repeat
+        thisExp.addData('mi_begin.started', globalClock.getTime())
+        # keep track of which components have finished
+        mi_beginComponents = [img_right]
+        for thisComponent in mi_beginComponents:
+            thisComponent.tStart = None
+            thisComponent.tStop = None
+            thisComponent.tStartRefresh = None
+            thisComponent.tStopRefresh = None
+            if hasattr(thisComponent, 'status'):
+                thisComponent.status = NOT_STARTED
+        # reset timers
+        t = 0
+        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+        frameN = -1
+        
+        # --- Run Routine "mi_begin" ---
+        routineForceEnded = not continueRoutine
+        while continueRoutine and routineTimer.getTime() < 5.0:
+            # get current time
+            t = routineTimer.getTime()
+            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+            # update/draw components on each frame
+            
+            # *img_right* updates
+            
+            # if img_right is starting this frame...
+            if img_right.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+                # keep track of start time/frame for later
+                img_right.frameNStart = frameN  # exact frame index
+                img_right.tStart = t  # local t and not account for scr refresh
+                img_right.tStartRefresh = tThisFlipGlobal  # on global time
+                win.timeOnFlip(img_right, 'tStartRefresh')  # time at next scr refresh
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'img_right.started')
+                # update status
+                img_right.status = STARTED
+                img_right.setAutoDraw(True)
+            
+            # if img_right is active this frame...
+            if img_right.status == STARTED:
+                # update params
+                pass
+            
+            # if img_right is stopping this frame...
+            if img_right.status == STARTED:
+                # is it time to stop? (based on global clock, using actual start)
+                if tThisFlipGlobal > img_right.tStartRefresh + 5-frameTolerance:
+                    # keep track of stop time/frame for later
+                    img_right.tStop = t  # not accounting for scr refresh
+                    img_right.frameNStop = frameN  # exact frame index
+                    # add timestamp to datafile
+                    thisExp.timestampOnFlip(win, 'img_right.stopped')
+                    # update status
+                    img_right.status = FINISHED
+                    img_right.setAutoDraw(False)
+            
+            # check for quit (typically the Esc key)
+            if defaultKeyboard.getKeys(keyList=["escape"]):
+                thisExp.status = FINISHED
+            if thisExp.status == FINISHED or endExpNow:
+                endExperiment(thisExp, inputs=inputs, win=win)
+                return
+            
+            # check if all components have finished
+            if not continueRoutine:  # a component has requested a forced-end of Routine
+                routineForceEnded = True
+                break
+            continueRoutine = False  # will revert to True if at least one component still running
+            for thisComponent in mi_beginComponents:
+                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                    continueRoutine = True
+                    break  # at least one component has not yet finished
+            
+            # refresh the screen
+            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+                win.flip()
+        
+        # --- Ending Routine "mi_begin" ---
+        for thisComponent in mi_beginComponents:
+            if hasattr(thisComponent, "setAutoDraw"):
+                thisComponent.setAutoDraw(False)
+        thisExp.addData('mi_begin.stopped', globalClock.getTime())
+        # Run 'End Routine' code from algo
+        # get data
+        data_from_buffer = receiver.get_data_from_buffer("classify_online")
+        if data_from_buffer["status"] == "ok":
+            raw_waves = data_from_buffer["data"].get_data()
+            timestamps = data_from_buffer["timestamp"]
+            # your process method ex:predict = pipeline(data)
+            predict = 1
+            if predict == 1:
+                # 气动手指令
+                feedback_time = 15
+            elif predict == 0:
+                # 气动手指令
+                feedback_time = 2
+        # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+        if routineForceEnded:
+            routineTimer.reset()
+        else:
+            routineTimer.addTime(-5.000000)
+        
+        # --- Prepare to start Routine "mi_feedback" ---
+        continueRoutine = True
+        # update component parameters for each repeat
+        thisExp.addData('mi_feedback.started', globalClock.getTime())
+        # keep track of which components have finished
+        mi_feedbackComponents = [feedback]
+        for thisComponent in mi_feedbackComponents:
+            thisComponent.tStart = None
+            thisComponent.tStop = None
+            thisComponent.tStartRefresh = None
+            thisComponent.tStopRefresh = None
+            if hasattr(thisComponent, 'status'):
+                thisComponent.status = NOT_STARTED
+        # reset timers
+        t = 0
+        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+        frameN = -1
+        
+        # --- Run Routine "mi_feedback" ---
+        routineForceEnded = not continueRoutine
+        while continueRoutine:
+            # get current time
+            t = routineTimer.getTime()
+            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+            # update/draw components on each frame
+            
+            # *feedback* updates
+            
+            # if feedback is starting this frame...
+            if feedback.status == NOT_STARTED and tThisFlip >= 0-frameTolerance:
+                # keep track of start time/frame for later
+                feedback.frameNStart = frameN  # exact frame index
+                feedback.tStart = t  # local t and not account for scr refresh
+                feedback.tStartRefresh = tThisFlipGlobal  # on global time
+                win.timeOnFlip(feedback, 'tStartRefresh')  # time at next scr refresh
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'feedback.started')
+                # update status
+                feedback.status = STARTED
+                feedback.setAutoDraw(True)
+            
+            # if feedback is active this frame...
+            if feedback.status == STARTED:
+                # update params
+                pass
+            
+            # if feedback is stopping this frame...
+            if feedback.status == STARTED:
+                # is it time to stop? (based on global clock, using actual start)
+                if tThisFlipGlobal > feedback.tStartRefresh + feedback_time-frameTolerance:
+                    # keep track of stop time/frame for later
+                    feedback.tStop = t  # not accounting for scr refresh
+                    feedback.frameNStop = frameN  # exact frame index
+                    # add timestamp to datafile
+                    thisExp.timestampOnFlip(win, 'feedback.stopped')
+                    # update status
+                    feedback.status = FINISHED
+                    feedback.setAutoDraw(False)
+            
+            # check for quit (typically the Esc key)
+            if defaultKeyboard.getKeys(keyList=["escape"]):
+                thisExp.status = FINISHED
+            if thisExp.status == FINISHED or endExpNow:
+                endExperiment(thisExp, inputs=inputs, win=win)
+                return
+            
+            # check if all components have finished
+            if not continueRoutine:  # a component has requested a forced-end of Routine
+                routineForceEnded = True
+                break
+            continueRoutine = False  # will revert to True if at least one component still running
+            for thisComponent in mi_feedbackComponents:
+                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                    continueRoutine = True
+                    break  # at least one component has not yet finished
+            
+            # refresh the screen
+            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+                win.flip()
+        
+        # --- Ending Routine "mi_feedback" ---
+        for thisComponent in mi_feedbackComponents:
+            if hasattr(thisComponent, "setAutoDraw"):
+                thisComponent.setAutoDraw(False)
+        thisExp.addData('mi_feedback.stopped', globalClock.getTime())
+        # the Routine "mi_feedback" was not non-slip safe, so reset the non-slip timer
+        routineTimer.reset()
+        
+        # --- Prepare to start Routine "mi_rest" ---
+        continueRoutine = True
+        # update component parameters for each repeat
+        thisExp.addData('mi_rest.started', globalClock.getTime())
+        # keep track of which components have finished
+        mi_restComponents = [img_rest]
+        for thisComponent in mi_restComponents:
+            thisComponent.tStart = None
+            thisComponent.tStop = None
+            thisComponent.tStartRefresh = None
+            thisComponent.tStopRefresh = None
+            if hasattr(thisComponent, 'status'):
+                thisComponent.status = NOT_STARTED
+        # reset timers
+        t = 0
+        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+        frameN = -1
+        
+        # --- Run Routine "mi_rest" ---
+        routineForceEnded = not continueRoutine
+        while continueRoutine and routineTimer.getTime() < 5.0:
+            # get current time
+            t = routineTimer.getTime()
+            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+            # update/draw components on each frame
+            
+            # *img_rest* updates
+            
+            # if img_rest is starting this frame...
+            if img_rest.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+                # keep track of start time/frame for later
+                img_rest.frameNStart = frameN  # exact frame index
+                img_rest.tStart = t  # local t and not account for scr refresh
+                img_rest.tStartRefresh = tThisFlipGlobal  # on global time
+                win.timeOnFlip(img_rest, 'tStartRefresh')  # time at next scr refresh
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'img_rest.started')
+                # update status
+                img_rest.status = STARTED
+                img_rest.setAutoDraw(True)
+            
+            # if img_rest is active this frame...
+            if img_rest.status == STARTED:
+                # update params
+                pass
+            
+            # if img_rest is stopping this frame...
+            if img_rest.status == STARTED:
+                # is it time to stop? (based on global clock, using actual start)
+                if tThisFlipGlobal > img_rest.tStartRefresh + 5-frameTolerance:
+                    # keep track of stop time/frame for later
+                    img_rest.tStop = t  # not accounting for scr refresh
+                    img_rest.frameNStop = frameN  # exact frame index
+                    # add timestamp to datafile
+                    thisExp.timestampOnFlip(win, 'img_rest.stopped')
+                    # update status
+                    img_rest.status = FINISHED
+                    img_rest.setAutoDraw(False)
+            
+            # check for quit (typically the Esc key)
+            if defaultKeyboard.getKeys(keyList=["escape"]):
+                thisExp.status = FINISHED
+            if thisExp.status == FINISHED or endExpNow:
+                endExperiment(thisExp, inputs=inputs, win=win)
+                return
+            
+            # check if all components have finished
+            if not continueRoutine:  # a component has requested a forced-end of Routine
+                routineForceEnded = True
+                break
+            continueRoutine = False  # will revert to True if at least one component still running
+            for thisComponent in mi_restComponents:
+                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                    continueRoutine = True
+                    break  # at least one component has not yet finished
+            
+            # refresh the screen
+            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+                win.flip()
+        
+        # --- Ending Routine "mi_rest" ---
+        for thisComponent in mi_restComponents:
+            if hasattr(thisComponent, "setAutoDraw"):
+                thisComponent.setAutoDraw(False)
+        thisExp.addData('mi_rest.stopped', globalClock.getTime())
+        # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+        if routineForceEnded:
+            routineTimer.reset()
+        else:
+            routineTimer.addTime(-5.000000)
+        thisExp.nextEntry()
+        
+        if thisSession is not None:
+            # if running in a Session with a Liaison client, send data up to now
+            thisSession.sendExperimentData()
+    # completed exp_train[1] repeats of 'trials'
+    
+    
+    # --- Prepare to start Routine "end" ---
+    continueRoutine = True
+    # update component parameters for each repeat
+    thisExp.addData('end.started', globalClock.getTime())
+    # keep track of which components have finished
+    endComponents = [mi_end]
+    for thisComponent in endComponents:
+        thisComponent.tStart = None
+        thisComponent.tStop = None
+        thisComponent.tStartRefresh = None
+        thisComponent.tStopRefresh = None
+        if hasattr(thisComponent, 'status'):
+            thisComponent.status = NOT_STARTED
+    # reset timers
+    t = 0
+    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
+    frameN = -1
+    
+    # --- Run Routine "end" ---
+    routineForceEnded = not continueRoutine
+    while continueRoutine and routineTimer.getTime() < 5.0:
+        # get current time
+        t = routineTimer.getTime()
+        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
+        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
+        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
+        # update/draw components on each frame
+        
+        # *mi_end* updates
+        
+        # if mi_end is starting this frame...
+        if mi_end.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
+            # keep track of start time/frame for later
+            mi_end.frameNStart = frameN  # exact frame index
+            mi_end.tStart = t  # local t and not account for scr refresh
+            mi_end.tStartRefresh = tThisFlipGlobal  # on global time
+            win.timeOnFlip(mi_end, 'tStartRefresh')  # time at next scr refresh
+            # add timestamp to datafile
+            thisExp.timestampOnFlip(win, 'mi_end.started')
+            # update status
+            mi_end.status = STARTED
+            mi_end.setAutoDraw(True)
+        
+        # if mi_end is active this frame...
+        if mi_end.status == STARTED:
+            # update params
+            pass
+        
+        # if mi_end is stopping this frame...
+        if mi_end.status == STARTED:
+            # is it time to stop? (based on global clock, using actual start)
+            if tThisFlipGlobal > mi_end.tStartRefresh + 5-frameTolerance:
+                # keep track of stop time/frame for later
+                mi_end.tStop = t  # not accounting for scr refresh
+                mi_end.frameNStop = frameN  # exact frame index
+                # add timestamp to datafile
+                thisExp.timestampOnFlip(win, 'mi_end.stopped')
+                # update status
+                mi_end.status = FINISHED
+                mi_end.setAutoDraw(False)
+        
+        # check for quit (typically the Esc key)
+        if defaultKeyboard.getKeys(keyList=["escape"]):
+            thisExp.status = FINISHED
+        if thisExp.status == FINISHED or endExpNow:
+            endExperiment(thisExp, inputs=inputs, win=win)
+            return
+        
+        # check if all components have finished
+        if not continueRoutine:  # a component has requested a forced-end of Routine
+            routineForceEnded = True
+            break
+        continueRoutine = False  # will revert to True if at least one component still running
+        for thisComponent in endComponents:
+            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
+                continueRoutine = True
+                break  # at least one component has not yet finished
+        
+        # refresh the screen
+        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
+            win.flip()
+    
+    # --- Ending Routine "end" ---
+    for thisComponent in endComponents:
+        if hasattr(thisComponent, "setAutoDraw"):
+            thisComponent.setAutoDraw(False)
+    thisExp.addData('end.stopped', globalClock.getTime())
+    # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
+    if routineForceEnded:
+        routineTimer.reset()
+    else:
+        routineTimer.addTime(-5.000000)
+    # Run 'End Experiment' code from code
+    receiver.stop_receive()
+    
+    # mark experiment as finished
+    endExperiment(thisExp, win=win, inputs=inputs)
+
+
+def saveData(thisExp):
+    """
+    Save data from this experiment
+    
+    Parameters
+    ==========
+    thisExp : psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    """
+    filename = thisExp.dataFileName
+    # these shouldn't be strictly necessary (should auto-save)
+    thisExp.saveAsWideText(filename + '.csv', delim='auto')
+    thisExp.saveAsPickle(filename)
+
+
+def endExperiment(thisExp, inputs=None, win=None):
+    """
+    End this experiment, performing final shut down operations.
+    
+    This function does NOT close the window or end the Python process - use `quit` for this.
+    
+    Parameters
+    ==========
+    thisExp : psychopy.data.ExperimentHandler
+        Handler object for this experiment, contains the data to save and information about 
+        where to save it to.
+    inputs : dict
+        Dictionary of input devices by name.
+    win : psychopy.visual.Window
+        Window for this experiment.
+    """
+    if win is not None:
+        # remove autodraw from all current components
+        win.clearAutoDraw()
+        # Flip one final time so any remaining win.callOnFlip() 
+        # and win.timeOnFlip() tasks get executed
+        win.flip()
+    # mark experiment handler as finished
+    thisExp.status = FINISHED
+    # shut down eyetracker, if there is one
+    if inputs is not None:
+        if 'eyetracker' in inputs and inputs['eyetracker'] is not None:
+            inputs['eyetracker'].setConnectionState(False)
+    logging.flush()
+
+
+def quit(thisExp, win=None, inputs=None, thisSession=None):
+    """
+    Fully quit, closing the window and ending the Python process.
+    
+    Parameters
+    ==========
+    win : psychopy.visual.Window
+        Window to close.
+    inputs : dict
+        Dictionary of input devices by name.
+    thisSession : psychopy.session.Session or None
+        Handle of the Session object this experiment is being run from, if any.
+    """
+    thisExp.abort()  # or data files will save again on exit
+    # make sure everything is closed down
+    if win is not None:
+        # Flip one final time so any remaining win.callOnFlip() 
+        # and win.timeOnFlip() tasks get executed before quitting
+        win.flip()
+        win.close()
+    if inputs is not None:
+        if 'eyetracker' in inputs and inputs['eyetracker'] is not None:
+            inputs['eyetracker'].setConnectionState(False)
+    logging.flush()
+    if thisSession is not None:
+        thisSession.stop()
+    # terminate Python process
+    core.quit()
+
+
+# if running this experiment as a script...
+if __name__ == '__main__':
+    # call all functions in order
+    expInfo = showExpInfoDlg(expInfo=expInfo)
+    thisExp = setupData(expInfo=expInfo)
+    logFile = setupLogging(filename=thisExp.dataFileName)
+    win = setupWindow(expInfo=expInfo)
+    inputs = setupInputs(expInfo=expInfo, thisExp=thisExp, win=win)
+    run(
+        expInfo=expInfo, 
+        thisExp=thisExp, 
+        win=win, 
+        inputs=inputs
+    )
+    saveData(thisExp=thisExp)
+    quit(thisExp=thisExp, win=win, inputs=inputs)

ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/MI_activity.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/backend_train_detail.svg


+ 109 - 0
docs/UML/dbschema.svg

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 7.0.4 (20221203.1631)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="469pt" height="284pt"
+ viewBox="0.00 0.00 469.00 284.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 280)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-280 465,-280 465,4 -4,4"/>
+<!-- dailystats -->
+<g id="node1" class="node">
+<title>dailystats</title>
+<text text-anchor="start" x="51" y="-35.4" font-family="Bitstream-Vera Sans" font-size="7.00">dailystats</text>
+<polygon fill="none" stroke="black" points="9,-29 9,-31 129,-31 129,-29 9,-29"/>
+<text text-anchor="start" x="11" y="-21.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; date : DATE</text>
+<text text-anchor="start" x="11" y="-9.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; create_subject_num : INTEGER</text>
+<polygon fill="none" stroke="black" points="8,-4 8,-44 130,-44 130,-4 8,-4"/>
+</g>
+<!-- fubopneumaticfinger -->
+<g id="node2" class="node">
+<title>fubopneumaticfinger</title>
+<text text-anchor="start" x="31" y="-251.4" font-family="Bitstream-Vera Sans" font-size="7.00">fubopneumaticfinger</text>
+<polygon fill="none" stroke="black" points="29,-245 29,-247 109,-247 109,-245 29,-245"/>
+<text text-anchor="start" x="31" y="-237.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; id : INTEGER</text>
+<text text-anchor="start" x="31" y="-225.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; train_id : INTEGER</text>
+<polygon fill="none" stroke="black" points="28,-220 28,-260 110,-260 110,-220 28,-220"/>
+</g>
+<!-- train -->
+<g id="node3" class="node">
+<title>train</title>
+<text text-anchor="start" x="234" y="-263.4" font-family="Bitstream-Vera Sans" font-size="7.00">train</text>
+<polygon fill="none" stroke="black" points="183,-257 183,-259 303,-259 303,-257 183,-257"/>
+<text text-anchor="start" x="185" y="-249.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; id : INTEGER</text>
+<text text-anchor="start" x="185" y="-237.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; position : VARCHAR</text>
+<text text-anchor="start" x="185" y="-225.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; rank : VARCHAR</text>
+<text text-anchor="start" x="185" y="-213.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; trial_num : INTEGER</text>
+<text text-anchor="start" x="185" y="-201.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; start_time : DATETIME</text>
+<text text-anchor="start" x="185" y="-189.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; end_time : DATETIME</text>
+<text text-anchor="start" x="185" y="-177.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; grade : VARCHAR(2)</text>
+<text text-anchor="start" x="185" y="-165.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; consume_time : INTEGER</text>
+<text text-anchor="start" x="185" y="-153.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; accuracy : FLOAT</text>
+<text text-anchor="start" x="185" y="-141.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; train_status : INTEGER</text>
+<text text-anchor="start" x="185" y="-129.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; eeg_device : VARCHAR(5)</text>
+<text text-anchor="start" x="185" y="-117.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; owner_id : INTEGER</text>
+<text text-anchor="start" x="185" y="-105.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; medical_certificate : VARCHAR</text>
+<polygon fill="none" stroke="black" points="182,-100 182,-272 304,-272 304,-100 182,-100"/>
+</g>
+<!-- fubopneumaticfinger&#45;&gt;train -->
+<g id="edge1" class="edge">
+<title>fubopneumaticfinger&#45;&gt;train</title>
+<path fill="none" stroke="black" d="M117.91,-224.96C132.72,-220.31 149.43,-215.06 165.66,-209.97"/>
+<ellipse fill="none" stroke="black" cx="169.81" cy="-208.67" rx="4" ry="4"/>
+<text text-anchor="middle" x="166.1" y="-201.72" font-family="Bitstream-Vera Sans" font-size="7.00">+ id</text>
+<text text-anchor="middle" x="136.41" y="-219.36" font-family="Bitstream-Vera Sans" font-size="7.00">+ train_id</text>
+</g>
+<!-- subject -->
+<g id="node4" class="node">
+<title>subject</title>
+<text text-anchor="start" x="391" y="-251.4" font-family="Bitstream-Vera Sans" font-size="7.00">subject</text>
+<polygon fill="none" stroke="black" points="357.5,-245 357.5,-247 452.5,-247 452.5,-245 357.5,-245"/>
+<text text-anchor="start" x="359.5" y="-237.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; id : VARCHAR(32)</text>
+<text text-anchor="start" x="359.5" y="-225.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; id_card : VARCHAR</text>
+<text text-anchor="start" x="359.5" y="-213.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; name : VARCHAR</text>
+<text text-anchor="start" x="359.5" y="-201.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; gender : VARCHAR</text>
+<text text-anchor="start" x="359.5" y="-189.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; birthday : DATE</text>
+<text text-anchor="start" x="359.5" y="-177.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; create_time : DATETIME</text>
+<text text-anchor="start" x="359.5" y="-165.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; left_hand : BOOLEAN</text>
+<text text-anchor="start" x="359.5" y="-153.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; right_hand : BOOLEAN</text>
+<text text-anchor="start" x="359.5" y="-141.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; left_leg : BOOLEAN</text>
+<text text-anchor="start" x="359.5" y="-129.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; right_leg : BOOLEAN</text>
+<text text-anchor="start" x="359.5" y="-117.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; remarks : VARCHAR</text>
+<polygon fill="none" stroke="black" points="356,-112 356,-260 453,-260 453,-112 356,-112"/>
+</g>
+<!-- train&#45;&gt;subject -->
+<g id="edge2" class="edge">
+<title>train&#45;&gt;subject</title>
+<path fill="none" stroke="black" d="M311.64,-186C320.81,-186 330.2,-186 339.31,-186"/>
+<ellipse fill="none" stroke="black" cx="343.8" cy="-186" rx="4" ry="4"/>
+<text text-anchor="middle" x="340.3" y="-188.4" font-family="Bitstream-Vera Sans" font-size="7.00">+ id</text>
+<text text-anchor="middle" x="332.14" y="-180.4" font-family="Bitstream-Vera Sans" font-size="7.00">+ owner_id</text>
+</g>
+<!-- handperipheral -->
+<g id="node5" class="node">
+<title>handperipheral</title>
+<text text-anchor="start" x="41.5" y="-185.4" font-family="Bitstream-Vera Sans" font-size="7.00">handperipheral</text>
+<polygon fill="none" stroke="black" points="22,-179 22,-181 117,-181 117,-179 22,-179"/>
+<text text-anchor="start" x="24" y="-171.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; id : INTEGER</text>
+<text text-anchor="start" x="24" y="-159.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; hand_select : INTEGER</text>
+<text text-anchor="start" x="24" y="-147.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; thumb : INTEGER</text>
+<text text-anchor="start" x="24" y="-135.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; index_finger : INTEGER</text>
+<text text-anchor="start" x="24" y="-123.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; middle_finger : INTEGER</text>
+<text text-anchor="start" x="24" y="-111.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; ring_finger : INTEGER</text>
+<text text-anchor="start" x="24" y="-99.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; little_finger : INTEGER</text>
+<text text-anchor="start" x="24" y="-87.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; duration : INTEGER</text>
+<text text-anchor="start" x="24" y="-75.4" font-family="Bitstream-Vera Sans" font-size="7.00">&#45; train_id : INTEGER</text>
+<polygon fill="none" stroke="black" points="20.5,-70 20.5,-194 117.5,-194 117.5,-70 20.5,-70"/>
+</g>
+<!-- handperipheral&#45;&gt;train -->
+<g id="edge3" class="edge">
+<title>handperipheral&#45;&gt;train</title>
+<path fill="none" stroke="black" d="M125.37,-149.38C138.22,-153.42 152.12,-157.78 165.69,-162.04"/>
+<ellipse fill="none" stroke="black" cx="169.79" cy="-163.33" rx="4" ry="4"/>
+<text text-anchor="middle" x="166.09" y="-159.08" font-family="Bitstream-Vera Sans" font-size="7.00">+ id</text>
+<text text-anchor="middle" x="143.87" y="-143.78" font-family="Bitstream-Vera Sans" font-size="7.00">+ train_id</text>
+</g>
+</g>
+</svg>

ファイルの差分が大きいため隠しています
+ 2 - 0
docs/UML/framework.svg


BIN
docs/UML/frontend_component.png


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/overview.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/page_activity_create_train.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/page_activity_home.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/page_activity_prepare_train.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/page_activity_subject_detail.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/route_eeg_activity.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/sig_chain sequence.svg


BIN
docs/UML/subject_sequence.png


BIN
docs/UML/train_sequence.png


ファイルの差分が大きいため隠しています
+ 3 - 0
docs/UML/usecase.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/外设_fubo_seq.svg


ファイルの差分が大きいため隠しています
+ 0 - 0
docs/UML/外设_ruishou_seq.svg


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません