Update
This commit is contained in:
commit
338917fdf7
129
.gitignore
vendored
Normal file
129
.gitignore
vendored
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
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/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.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
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__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/
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
178
README.md
Normal file
178
README.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<div align="center">
|
||||||
|
<br>
|
||||||
|
<img src="images/icon.png" alt="Fay">
|
||||||
|
<h1>FAY</h1>
|
||||||
|
<h3>数 字 人 控 制 器</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
本开源项目名为“数字人控制器”。意为,本项目可以充当时下流行的虚拟人、虚拟主播、数字人,等仿人形数字形象的内核部分。
|
||||||
|
|
||||||
|
使用UE、C4D、DAZ、LIVE2D等三维引擎软件开发的数字形象可以与本“数字人控制器”对接,从而实现虚拟主播、数字导游、数字助手等。我们提供UE4对接的demo,但我们更鼓励用户自行实现喜欢的数字形象。
|
||||||
|
|
||||||
|
当然,若不考虑外观形象的话,本“数字人控制器”其实也可以独立使用的,可以充当一个语音助理。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 环境
|
||||||
|
|
||||||
|
- Python 3.8.0 +
|
||||||
|
|
||||||
|
- Chrome 浏览器 (若不开启直播功能,可跳过)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 配置 ChromeDriver (若不开启直播功能,可跳过)
|
||||||
|
|
||||||
|
1. Chrome 浏览器进入 [`chrome://settings/help`](chrome://settings/help) 查看当前版本
|
||||||
|
2. 下载对应版本 [ChromeDriver](https://chromedriver.chromium.org/downloads)
|
||||||
|
3. 解压zip并拷贝至 <u>./bin</u> 目录
|
||||||
|
4. 编辑 <u>system.conf</u> 配置 ChromeDriver 路径
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 配置应用密钥
|
||||||
|
|
||||||
|
1. 查看 [AI 模块](#ai-模块)
|
||||||
|
|
||||||
|
2. 浏览链接,注册并创建应用,将应用密钥填入 `./system.conf` 中
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
|
启动数字人图像控制器
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 图形界面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 人设
|
||||||
|
|
||||||
|
数字人属性,与用户交互中能做出相应的响应。
|
||||||
|
|
||||||
|
##### 交互灵敏度
|
||||||
|
|
||||||
|
在交互中,数字人能感受用户的情感,并作出反应。最直的体现,就是语气的变化,如 开心/伤心/生气 等。
|
||||||
|
|
||||||
|
设置灵敏度,可改变用户情感对于数字人的影响程度。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 接收来源
|
||||||
|
|
||||||
|
#### 抖音
|
||||||
|
|
||||||
|
填入直播间地址,实现与直播间粉丝交互
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 麦克风
|
||||||
|
|
||||||
|
选择麦克风设备,实现面对面交互,成为你的伙伴
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 商品栏
|
||||||
|
|
||||||
|
填入商品介绍,数字人将自动讲解商品。
|
||||||
|
|
||||||
|
当用户对商品有疑问时,数字人可自动跳转至对应商品并解答问题。
|
||||||
|
|
||||||
|
配合抖音接收来源,实现直播间自动带货。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## AI 模块
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
启动前需填入应用密钥
|
||||||
|
|
||||||
|
| 模块 | 描述 | 链接 |
|
||||||
|
| ------------------------- | -------------------------- | ------------------------------------------------------------ |
|
||||||
|
| ./ai_module/ali_nls.py | 阿里云 实时语音识别 | https://ai.aliyun.com/nls/trans |
|
||||||
|
| ./ai_module/ms_tts_sdk.py | 微软 文本转语音 基于SDK | https://azure.microsoft.com/zh-cn/services/cognitive-services/text-to-speech/ |
|
||||||
|
| ./ai_module/xf_aiui.py | 讯飞 人机交互-自然语言处理 | https://aiui.xfyun.cn/solution/webapi |
|
||||||
|
| ./ai_module/xf_ltp.py | 讯飞 情感分析 | https://www.xfyun.cn/service/emotion-analysis |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 与数字形象通讯(非必须)
|
||||||
|
|
||||||
|
控制器与采用 WebSocket 方式与 UE 通讯
|
||||||
|
|
||||||
|
通讯地址: [`ws://127.0.0.1:10002`](ws://127.0.0.1:10002)
|
||||||
|
|
||||||
|
消息格式: 查看 [WebSocket.md](https://github.com/TheRamU/Fay/blob/main/WebSocket.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── main.py # 程序主入口
|
||||||
|
├── fay_booter.py # 核心启动模块
|
||||||
|
├── config.json # 控制器配置文件
|
||||||
|
├── system.conf # 系统配置文件
|
||||||
|
├── ai_module
|
||||||
|
│ ├── ali_nls.py # 阿里云 实时语音
|
||||||
|
│ ├── ms_tts_sdk.py # 微软 文本转语音
|
||||||
|
│ ├── xf_aiui.py # 讯飞 人机交互-自然语言处理
|
||||||
|
│ └── xf_ltp.py # 讯飞 性感分析
|
||||||
|
├── bin # 可执行文件目录
|
||||||
|
├── core # 数字人核心
|
||||||
|
│ ├── fay_core.py # 数字人核心模块
|
||||||
|
│ ├── recorder.py # 录音器
|
||||||
|
│ ├── tts_voice.py # 语音生源枚举
|
||||||
|
│ ├── viewer.py # 抖音直播间接入模块
|
||||||
|
│ └── wsa_server.py # WebSocket 服务端
|
||||||
|
├── gui # 图形界面
|
||||||
|
│ ├── flask_server.py # Flask 服务端
|
||||||
|
│ ├── static
|
||||||
|
│ ├── templates
|
||||||
|
│ └── window.py # 窗口模块
|
||||||
|
├── scheduler
|
||||||
|
│ └── thread_manager.py # 调度管理器
|
||||||
|
└── utils # 工具模块
|
||||||
|
├── config_util.py
|
||||||
|
├── storer.py
|
||||||
|
└── util.py
|
||||||
|
```
|
||||||
|
|
50
WebSocket.md
Normal file
50
WebSocket.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
## 消息格式
|
||||||
|
|
||||||
|
通讯地址: [`ws://127.0.0.1:10002`](ws://127.0.0.1:10002)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 发送情绪值
|
||||||
|
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"Topic": "Unreal",
|
||||||
|
"Data": {
|
||||||
|
"Key": "mood",
|
||||||
|
"Value": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| 参数 | 描述 | 类型 | 范围 |
|
||||||
|
| ---------- | ------ | ----- | ------- |
|
||||||
|
| Data.Value | 情绪值 | float | [-1, 1] |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 发送音频
|
||||||
|
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"Topic": "Unreal",
|
||||||
|
"Data": {
|
||||||
|
"Key": "audio",
|
||||||
|
"Value": "C:\samples\sample-1.mp3",
|
||||||
|
"Time": 10,
|
||||||
|
"Type": "interact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| 参数 | 描述 | 类型 | 范围 |
|
||||||
|
| ---------- | ---------------- | ----- | --------------- |
|
||||||
|
| Data.Value | 音频文件绝对路径 | str | |
|
||||||
|
| Data.Time | 音频时长 (秒) | float | |
|
||||||
|
| Data.Type | 发言类型 | str | interact/script |
|
||||||
|
|
173
ai_module/ali_nls.py
Normal file
173
ai_module/ali_nls.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
import websocket
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import ssl
|
||||||
|
import _thread as thread
|
||||||
|
from aliyunsdkcore.client import AcsClient
|
||||||
|
from aliyunsdkcore.request import CommonRequest
|
||||||
|
|
||||||
|
from core import wsa_server
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import util
|
||||||
|
from utils import config_util as cfg
|
||||||
|
|
||||||
|
__running = True
|
||||||
|
__my_thread = None
|
||||||
|
_token = ''
|
||||||
|
|
||||||
|
|
||||||
|
def __post_token():
|
||||||
|
global _token
|
||||||
|
__client = AcsClient(
|
||||||
|
cfg.key_ali_nls_key_id,
|
||||||
|
cfg.key_ali_nls_key_secret,
|
||||||
|
"cn-shanghai"
|
||||||
|
)
|
||||||
|
|
||||||
|
__request = CommonRequest()
|
||||||
|
__request.set_method('POST')
|
||||||
|
__request.set_domain('nls-meta.cn-shanghai.aliyuncs.com')
|
||||||
|
__request.set_version('2019-02-28')
|
||||||
|
__request.set_action_name('CreateToken')
|
||||||
|
_token = json.loads(__client.do_action_with_exception(__request))['Token']['Id']
|
||||||
|
|
||||||
|
|
||||||
|
def __runnable():
|
||||||
|
while __running:
|
||||||
|
__post_token()
|
||||||
|
time.sleep(60 * 60 * 12)
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
MyThread(target=__runnable).start()
|
||||||
|
|
||||||
|
|
||||||
|
class ALiNls:
|
||||||
|
# 初始化
|
||||||
|
def __init__(self):
|
||||||
|
self.__URL = 'wss://nls-gateway-cn-shenzhen.aliyuncs.com/ws/v1'
|
||||||
|
self.__ws = None
|
||||||
|
self.__connected = False
|
||||||
|
self.__frames = []
|
||||||
|
self.__state = 0
|
||||||
|
self.__closing = False
|
||||||
|
self.__task_id = ''
|
||||||
|
self.done = False
|
||||||
|
self.finalResults = ""
|
||||||
|
|
||||||
|
def __create_header(self, name):
|
||||||
|
if name == 'StartTranscription':
|
||||||
|
self.__task_id = util.random_hex(32)
|
||||||
|
header = {
|
||||||
|
"appkey": cfg.key_ali_nls_app_key,
|
||||||
|
"message_id": util.random_hex(32),
|
||||||
|
"task_id": self.__task_id,
|
||||||
|
"namespace": "SpeechTranscriber",
|
||||||
|
"name": name
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
|
||||||
|
# 收到websocket消息的处理
|
||||||
|
def on_message(self, ws, message):
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
header = data['header']
|
||||||
|
name = header['name']
|
||||||
|
if name == 'SentenceEnd':
|
||||||
|
self.done = True
|
||||||
|
self.finalResults = data['payload']['result']
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": self.finalResults})
|
||||||
|
elif name == 'TranscriptionResultChanged':
|
||||||
|
self.finalResults = data['payload']['result']
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": self.finalResults})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
# print("### message:", message)
|
||||||
|
if self.__closing:
|
||||||
|
try:
|
||||||
|
self.__ws.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# 收到websocket错误的处理
|
||||||
|
def on_close(self, ws, code, msg):
|
||||||
|
self.__connected = False
|
||||||
|
print("### CLOSE:", msg)
|
||||||
|
|
||||||
|
# 收到websocket错误的处理
|
||||||
|
def on_error(self, ws, error):
|
||||||
|
print("### error:", error)
|
||||||
|
|
||||||
|
# 收到websocket连接建立的处理
|
||||||
|
def on_open(self, ws):
|
||||||
|
self.__connected = True
|
||||||
|
|
||||||
|
# print("连接上了!!!")
|
||||||
|
|
||||||
|
def run(*args):
|
||||||
|
while self.__connected:
|
||||||
|
try:
|
||||||
|
if len(self.__frames) > 0:
|
||||||
|
frame = self.__frames[0]
|
||||||
|
self.__frames.pop(0)
|
||||||
|
if type(frame) == dict:
|
||||||
|
ws.send(json.dumps(frame))
|
||||||
|
elif type(frame) == bytes:
|
||||||
|
ws.send(frame, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
# print('发送 ------> ' + str(type(frame)))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
time.sleep(0.04)
|
||||||
|
|
||||||
|
thread.start_new_thread(run, ())
|
||||||
|
|
||||||
|
def __connect(self):
|
||||||
|
self.finalResults = ""
|
||||||
|
self.done = False
|
||||||
|
self.__frames.clear()
|
||||||
|
websocket.enableTrace(False)
|
||||||
|
self.__ws = websocket.WebSocketApp(self.__URL + '?token=' + _token, on_message=self.on_message)
|
||||||
|
self.__ws.on_open = self.on_open
|
||||||
|
self.__ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||||
|
|
||||||
|
def add_frame(self, frame):
|
||||||
|
self.__frames.append(frame)
|
||||||
|
|
||||||
|
def send(self, buf):
|
||||||
|
self.__frames.append(buf)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
Thread(target=self.__connect, args=[]).start()
|
||||||
|
data = {
|
||||||
|
'header': self.__create_header('StartTranscription'),
|
||||||
|
"payload": {
|
||||||
|
"format": "pcm",
|
||||||
|
"sample_rate": 16000,
|
||||||
|
"enable_intermediate_result": True,
|
||||||
|
"enable_punctuation_prediction": False,
|
||||||
|
"enable_inverse_text_normalization": True,
|
||||||
|
"speech_noise_threshold": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.add_frame(data)
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
if self.__connected:
|
||||||
|
try:
|
||||||
|
for frame in self.__frames:
|
||||||
|
self.__frames.pop(0)
|
||||||
|
if type(frame) == dict:
|
||||||
|
self.__ws.send(json.dumps(frame))
|
||||||
|
elif type(frame) == bytes:
|
||||||
|
self.__ws.send(frame, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
time.sleep(0.4)
|
||||||
|
self.__frames.clear()
|
||||||
|
frame = {"header": self.__create_header('StopTranscription')}
|
||||||
|
self.__ws.send(json.dumps(frame))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
self.__closing = True
|
||||||
|
self.__connected = False
|
68
ai_module/ms_tts_sdk.py
Normal file
68
ai_module/ms_tts_sdk.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import azure.cognitiveservices.speech as speechsdk
|
||||||
|
|
||||||
|
from core import tts_voice
|
||||||
|
from core.tts_voice import EnumVoice
|
||||||
|
from utils import util, config_util
|
||||||
|
from utils import config_util as cfg
|
||||||
|
|
||||||
|
|
||||||
|
class Speech:
|
||||||
|
def __init__(self):
|
||||||
|
self.__speech_config = speechsdk.SpeechConfig(subscription=cfg.key_ms_tts_key, region="eastasia")
|
||||||
|
self.__speech_config.speech_recognition_language = "zh-CN"
|
||||||
|
self.__speech_config.speech_synthesis_voice_name = "zh-CN-XiaoxiaoNeural"
|
||||||
|
self.__speech_config.set_speech_synthesis_output_format(speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3)
|
||||||
|
self.__synthesizer = speechsdk.SpeechSynthesizer(speech_config=self.__speech_config, audio_config=None)
|
||||||
|
self.__connection = None
|
||||||
|
self.__history_data = []
|
||||||
|
|
||||||
|
def __get_history(self, voice_name, style, text):
|
||||||
|
for data in self.__history_data:
|
||||||
|
if data[0] == voice_name and data[1] == style and data[2] == text:
|
||||||
|
return data[3]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.__connection = speechsdk.Connection.from_speech_synthesizer(self.__synthesizer)
|
||||||
|
self.__connection.open(True)
|
||||||
|
util.log(1, "TTS 服务已经连接!")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.__connection is not None:
|
||||||
|
self.__connection.close()
|
||||||
|
|
||||||
|
"""
|
||||||
|
文字转语音
|
||||||
|
:param text: 文本信息
|
||||||
|
:param style: 说话风格、语气
|
||||||
|
:returns: 音频文件路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_sample(self, text, style):
|
||||||
|
voice_type = tts_voice.get_voice_of(config_util.config["attribute"]["voice"])
|
||||||
|
voice_name = EnumVoice.XIAO_XIAO.value["voiceName"]
|
||||||
|
if voice_type is not None:
|
||||||
|
voice_name = voice_type.value["voiceName"]
|
||||||
|
history = self.__get_history(voice_name, style, text)
|
||||||
|
if history is not None:
|
||||||
|
return history
|
||||||
|
ssml = '<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">' \
|
||||||
|
'<voice name="{}">' \
|
||||||
|
'<mstts:express-as style="{}" styledegree="{}">' \
|
||||||
|
'{}' \
|
||||||
|
'</mstts:express-as>' \
|
||||||
|
'</voice>' \
|
||||||
|
'</speak>'.format(voice_name, style, 1.8, text)
|
||||||
|
result = self.__synthesizer.speak_ssml(ssml)
|
||||||
|
audio_data_stream = speechsdk.AudioDataStream(result)
|
||||||
|
file_url = './samples/sample-' + str(int(time.time() * 1000)) + '.mp3'
|
||||||
|
audio_data_stream.save_to_wav_file(file_url)
|
||||||
|
if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
|
||||||
|
self.__history_data.append((voice_name, style, text, file_url))
|
||||||
|
return file_url
|
||||||
|
else:
|
||||||
|
util.log(1, "[x] 语音转换失败!")
|
||||||
|
util.log(1, "[x] 原因: " + str(result.reason))
|
||||||
|
return None
|
107
ai_module/xf_aiui.py
Normal file
107
ai_module/xf_aiui.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ws4py.client.threadedclient import WebSocketClient
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import uuid
|
||||||
|
from utils import config_util as cfg
|
||||||
|
|
||||||
|
base_url = "ws://wsapi.xfyun.cn/v1/aiui"
|
||||||
|
|
||||||
|
end_tag = "--end--"
|
||||||
|
|
||||||
|
|
||||||
|
# qa 通讯类
|
||||||
|
class __WSClient(WebSocketClient):
|
||||||
|
q_msg = ''
|
||||||
|
a_msg = ''
|
||||||
|
|
||||||
|
def opened(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def closed(self, code, reason=None):
|
||||||
|
# if code == 1000:
|
||||||
|
# print("qa close")
|
||||||
|
# else:
|
||||||
|
# print("连接异常关闭,code:" + str(code) + " ,reason:" + str(reason))
|
||||||
|
return
|
||||||
|
|
||||||
|
def received_message(self, m):
|
||||||
|
|
||||||
|
s = json.loads(str(m))
|
||||||
|
|
||||||
|
if s['action'] == "started":
|
||||||
|
|
||||||
|
# 输入内容并发送
|
||||||
|
str_content = self.q_msg
|
||||||
|
self.send(bytes(str_content.encode('utf-8')))
|
||||||
|
time.sleep(0.04)
|
||||||
|
|
||||||
|
# 数据发送结束之后发送结束标识
|
||||||
|
self.send(bytes(end_tag.encode("utf-8")))
|
||||||
|
|
||||||
|
elif s['action'] == "result":
|
||||||
|
data = s['data']
|
||||||
|
# with open('qa/out.txt', 'w') as file:
|
||||||
|
# file.write(str(data))
|
||||||
|
if data['sub'] == "iat":
|
||||||
|
print("user: ", data["text"])
|
||||||
|
elif data['sub'] == "nlp":
|
||||||
|
intent = data['intent']
|
||||||
|
if intent['rc'] == 0:
|
||||||
|
self.a_msg = intent['answer']['text']
|
||||||
|
else:
|
||||||
|
self.a_msg = "我没有理解你说的话啊"
|
||||||
|
elif data['sub'] == "tts":
|
||||||
|
# TODO 播报pcm音频
|
||||||
|
print('tts')
|
||||||
|
pass
|
||||||
|
elif s['action'] == "error":
|
||||||
|
print('[NLP错误] ' + s['desc'])
|
||||||
|
else:
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_auth_id():
|
||||||
|
mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
|
||||||
|
return hashlib.md5(":".join([mac[e:e + 2] for e in range(0, 11, 2)]).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def question(text):
|
||||||
|
ws = None
|
||||||
|
try:
|
||||||
|
# 构造握手参数
|
||||||
|
curTime = int(time.time())
|
||||||
|
|
||||||
|
auth_id = __get_auth_id()
|
||||||
|
|
||||||
|
param = """{{
|
||||||
|
"auth_id": "{0}",
|
||||||
|
"data_type": "text",
|
||||||
|
"scene": "main_box",
|
||||||
|
"ver_type": "monitor",
|
||||||
|
"close_delay": "200",
|
||||||
|
"ent":"xtts",
|
||||||
|
"vcn":"x_xiaoyan",
|
||||||
|
"speed":"50",
|
||||||
|
"interact_mode":"continuous",
|
||||||
|
"context": "{{\\\"sdk_support\\\":[\\\"iat\\\",\\\"nlp\\\",\\\"tts\\\"]}}"
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
param = param.format(auth_id).encode(encoding="utf-8")
|
||||||
|
paramBase64 = base64.b64encode(param).decode()
|
||||||
|
checkSumPre = cfg.key_xf_aiui_api_key + str(curTime) + paramBase64
|
||||||
|
checksum = hashlib.md5(checkSumPre.encode("utf-8")).hexdigest()
|
||||||
|
connParam = "?appid=" + cfg.key_xf_aiui_app_id + "&checksum=" + checksum + "¶m=" + paramBase64 + "&curtime=" + str(curTime) + "&signtype=md5"
|
||||||
|
|
||||||
|
ws = __WSClient(base_url + connParam, protocols=['chat'], headers=[("Origin", "https://wsapi.xfyun.cn")])
|
||||||
|
ws.q_msg = text
|
||||||
|
ws.connect()
|
||||||
|
ws.run_forever()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if ws is not None:
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return ws.a_msg
|
59
ai_module/xf_ltp.py
Normal file
59
ai_module/xf_ltp.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
from utils import config_util as cfg
|
||||||
|
|
||||||
|
__URL = "https://ltpapi.xfyun.cn/v2/sa"
|
||||||
|
|
||||||
|
|
||||||
|
def __quest(text):
|
||||||
|
body = urllib.parse.urlencode({'text': text}).encode('utf-8')
|
||||||
|
param = {"type": "dependent"}
|
||||||
|
x_param = base64.b64encode(json.dumps(param).replace(' ', '').encode('utf-8'))
|
||||||
|
x_time = str(int(time.time()))
|
||||||
|
x_checksum = hashlib.md5(cfg.key_xf_ltp_api_key.encode('utf-8') + str(x_time).encode('utf-8') + x_param).hexdigest()
|
||||||
|
x_header = {
|
||||||
|
'X-Appid': cfg.key_xf_ltp_app_id,
|
||||||
|
'X-CurTime': x_time,
|
||||||
|
'X-Param': x_param,
|
||||||
|
'X-CheckSum': x_checksum
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(__URL, body, x_header)
|
||||||
|
result = urllib.request.urlopen(req)
|
||||||
|
result = result.read()
|
||||||
|
return json.loads(result.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
情感分析
|
||||||
|
|
||||||
|
:param text: 文本
|
||||||
|
|
||||||
|
:returns: 情感分数 (0.7以上为褒义, 0.3-0.7为中性 0.3以下为贬义,, -1为分析失败)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_score(text):
|
||||||
|
result = __quest(text)
|
||||||
|
if result['desc'] == 'success':
|
||||||
|
return float(result['data']['score'])
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
情感分析
|
||||||
|
|
||||||
|
:param text: 文本
|
||||||
|
|
||||||
|
:returns: 情感极性分类 (2为褒义, 1为中性 0为贬义,, -1为分析失败)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_sentiment(text):
|
||||||
|
result = __quest(text)
|
||||||
|
if result['desc'] == 'success':
|
||||||
|
return int(result['data']['sentiment']) + 1
|
||||||
|
return -1
|
52
config.json
Normal file
52
config.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"attribute": {
|
||||||
|
"age": "成年",
|
||||||
|
"birth": "中国",
|
||||||
|
"constellation": "水瓶座",
|
||||||
|
"contact": "微信123456789",
|
||||||
|
"gender": "男",
|
||||||
|
"hobby": "发呆",
|
||||||
|
"job": "产品布道者",
|
||||||
|
"name": "陈升",
|
||||||
|
"voice": "YUN_XI",
|
||||||
|
"zodiac": "蛇"
|
||||||
|
},
|
||||||
|
"interact": {
|
||||||
|
"QnA": "E:/QnA/全局QnA.xlsx",
|
||||||
|
"maxInteractTime": 15,
|
||||||
|
"perception": {
|
||||||
|
"chat": 7,
|
||||||
|
"follow": 10,
|
||||||
|
"gift": 50,
|
||||||
|
"indifferent": 10,
|
||||||
|
"join": 10
|
||||||
|
},
|
||||||
|
"playSound": false
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"QnA": "E:/QnA/商品QnA.xlsx",
|
||||||
|
"demoVideo": "C:/Demo.mp4",
|
||||||
|
"enabled": false,
|
||||||
|
"explain": {
|
||||||
|
"character": "",
|
||||||
|
"discount": "",
|
||||||
|
"intro": "",
|
||||||
|
"price": "",
|
||||||
|
"promise": "",
|
||||||
|
"usage": ""
|
||||||
|
},
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": {
|
||||||
|
"liveRoom": {
|
||||||
|
"enabled": false,
|
||||||
|
"url": "https://live.douyin.com/"
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"device": "",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
504
core/fay_core.py
Normal file
504
core/fay_core.py
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
import difflib
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import wave
|
||||||
|
|
||||||
|
import eyed3
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
# 适应模型使用
|
||||||
|
import numpy as np
|
||||||
|
# import tensorflow as tf
|
||||||
|
|
||||||
|
from ai_module import xf_aiui
|
||||||
|
from ai_module import xf_ltp
|
||||||
|
from ai_module.ms_tts_sdk import Speech
|
||||||
|
from core import wsa_server, tts_voice
|
||||||
|
from core.tts_voice import EnumVoice
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import util, storer, config_util
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
|
||||||
|
class FeiFei:
|
||||||
|
def __init__(self):
|
||||||
|
pygame.init()
|
||||||
|
self.q_msg = '你叫什么名字?'
|
||||||
|
self.a_msg = 'hi,我叫菲菲,英文名是fay'
|
||||||
|
self.mood = 0.0 # 情绪值
|
||||||
|
self.item_index = 0
|
||||||
|
|
||||||
|
self.X = np.array([1, 0, 0, 0, 0, 0, 0, 0]).reshape(1, -1) # 适应模型变量矩阵
|
||||||
|
# self.W = np.array([0.01577594,1.16119452,0.75828,0.207746,1.25017864,0.1044121,0.4294899,0.2770932]).reshape(-1,1) #适应模型变量矩阵
|
||||||
|
self.W = np.array([0.0, 0.6, 0.1, 0.7, 0.3, 0.0, 0.0, 0.0]).reshape(-1, 1) # 适应模型变量矩阵
|
||||||
|
|
||||||
|
# 人设提问关键字
|
||||||
|
self.attribute_keyword = [
|
||||||
|
[['你叫什么名字', '你的名字是什么'], 'name'],
|
||||||
|
[['你是男的还是女的', '你是男生还是女生', '你的性别是什么', '你是男生吗', '你是女生吗', '你是男的吗', '你是女的吗', '你是男孩子吗', '你是女孩子吗', ], 'gender', ],
|
||||||
|
[['你今年多大了', '你多大了', '你今年多少岁', '你几岁了', '你今年几岁了', '你今年几岁了', '你什么时候出生', '你的生日是什么', '你的年龄'], 'age', ],
|
||||||
|
[['你的家乡在哪', '你的家乡是什么', '你家在哪', '你住在哪', '你出生在哪', '你的出生地在哪', '你的出生地是什么', ], 'birth', ],
|
||||||
|
[['你的生肖是什么', '你属什么', ], 'zodiac', ],
|
||||||
|
[['你是什么座', '你是什么星座', '你的星座是什么', ], 'constellation', ],
|
||||||
|
[['你是做什么的', '你的职业是什么', '你是干什么的', '你的职位是什么', '你的工作是什么', '你是做什么工作的'], 'job', ],
|
||||||
|
[['你的爱好是什么', '你有爱好吗', '你喜欢什么', '你喜欢做什么'], 'hobby'],
|
||||||
|
[['联系方式', '联系你们', '怎么联系客服', '有没有客服'], 'contact']
|
||||||
|
]
|
||||||
|
|
||||||
|
# 商品提问关键字
|
||||||
|
self.explain_keyword = [
|
||||||
|
[['是什么'], 'intro'],
|
||||||
|
[['怎么用', '使用场景', '有什么作用'], 'usage'],
|
||||||
|
[['怎么卖', '多少钱', '售价'], 'price'],
|
||||||
|
[['便宜点', '优惠', '折扣', '促销'], 'discount'],
|
||||||
|
[['质量', '保证', '担保'], 'promise'],
|
||||||
|
[['特点', '优点'], 'character'],
|
||||||
|
]
|
||||||
|
|
||||||
|
self.wsParam = None
|
||||||
|
self.wss = None
|
||||||
|
self.sp = Speech()
|
||||||
|
self.speaking = False
|
||||||
|
self.last_interact_time = time.time()
|
||||||
|
self.last_speak_data = ''
|
||||||
|
self.interactive = []
|
||||||
|
self.sleep = False
|
||||||
|
self.__running = True
|
||||||
|
self.sp.connect() # 预连接
|
||||||
|
self.last_quest_time = time.time()
|
||||||
|
|
||||||
|
def __string_similar(self, s1, s2):
|
||||||
|
return difflib.SequenceMatcher(None, s1, s2).quick_ratio()
|
||||||
|
|
||||||
|
def __read_qna(self, filename) -> list:
|
||||||
|
qna = []
|
||||||
|
try:
|
||||||
|
wb = load_workbook(filename)
|
||||||
|
sheets = wb.worksheets # 获取当前所有的sheet
|
||||||
|
sheet = sheets[0]
|
||||||
|
for row in sheet.rows:
|
||||||
|
if len(row) >= 2:
|
||||||
|
qna.append([row[0].value.split(";"), row[1].value])
|
||||||
|
except BaseException as e:
|
||||||
|
print("无法读取Q&A文件 {} -> ".format(filename) + str(e))
|
||||||
|
return qna
|
||||||
|
|
||||||
|
def __get_keyword(self, keyword_dict, text):
|
||||||
|
last_similar = 0
|
||||||
|
last_answer = ''
|
||||||
|
for qa in keyword_dict:
|
||||||
|
for quest in qa[0]:
|
||||||
|
similar = self.__string_similar(text, quest)
|
||||||
|
if quest in text:
|
||||||
|
similar += 0.3
|
||||||
|
if similar > last_similar:
|
||||||
|
last_similar = similar
|
||||||
|
last_answer = qa[1]
|
||||||
|
if last_similar >= 0.6:
|
||||||
|
return last_answer
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __get_answer(self, text):
|
||||||
|
|
||||||
|
# 人设问答
|
||||||
|
keyword = self.__get_keyword(self.attribute_keyword, text)
|
||||||
|
if keyword is not None:
|
||||||
|
return config_util.config["attribute"][keyword]
|
||||||
|
|
||||||
|
# 全局问答
|
||||||
|
answer = self.__get_keyword(self.__read_qna(config_util.config['interact']['QnA']), text)
|
||||||
|
if answer is not None:
|
||||||
|
return answer
|
||||||
|
|
||||||
|
items = self.__get_item_list()
|
||||||
|
|
||||||
|
if len(items) > 0:
|
||||||
|
item = items[self.item_index]
|
||||||
|
|
||||||
|
# 跨商品物品问答匹配
|
||||||
|
for ite in items:
|
||||||
|
name = ite["name"]
|
||||||
|
if name != item["name"]:
|
||||||
|
if name in text or self.__string_similar(text, name) > 0.6:
|
||||||
|
item = ite
|
||||||
|
break
|
||||||
|
|
||||||
|
# 商品介绍问答
|
||||||
|
keyword = self.__get_keyword(self.explain_keyword, text)
|
||||||
|
if keyword is not None:
|
||||||
|
try:
|
||||||
|
return item["explain"][keyword]
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# 商品问答
|
||||||
|
answer = self.__get_keyword(self.__read_qna(item["QnA"]), text)
|
||||||
|
if answer is not None:
|
||||||
|
return answer
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __get_list_answer(self, answers, text):
|
||||||
|
last_similar = 0
|
||||||
|
last_answer = ''
|
||||||
|
for mlist in answers:
|
||||||
|
for quest in mlist[0]:
|
||||||
|
similar = self.__string_similar(text, quest)
|
||||||
|
if quest in text:
|
||||||
|
similar += 0.3
|
||||||
|
if similar > last_similar:
|
||||||
|
last_similar = similar
|
||||||
|
answer_list = mlist[1]
|
||||||
|
last_answer = answer_list[random.randint(0, len(answer_list) - 1)]
|
||||||
|
# print("相似度: {}, 回答: {}".format(last_similar, last_answer))
|
||||||
|
if last_similar >= 0.6:
|
||||||
|
return last_answer
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __auto_speak(self):
|
||||||
|
i = 0
|
||||||
|
script_index = 0
|
||||||
|
while self.__running:
|
||||||
|
time.sleep(0.8)
|
||||||
|
if self.speaking or self.sleep:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 简化逻辑:默认执行带货脚本,带货脚本执行其间有人互动,则执行完当前脚本就回应最后三条互动,回应完继续执行带货脚本
|
||||||
|
if i <= 3 and len(self.interactive) > i:
|
||||||
|
i += 1
|
||||||
|
interact = self.interactive[0 - i]
|
||||||
|
if interact[0] == 1:
|
||||||
|
self.q_msg = interact[2]
|
||||||
|
index = interact[0]
|
||||||
|
# print("index:{0}".format(index))
|
||||||
|
user_name = interact[1]
|
||||||
|
# self.__isExecute = True #!!!!
|
||||||
|
|
||||||
|
if index == 1:
|
||||||
|
answer = self.__get_answer(self.q_msg)
|
||||||
|
text = ''
|
||||||
|
if answer is None:
|
||||||
|
try:
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": "思考中..."})
|
||||||
|
util.log(1, '自然语言处理...')
|
||||||
|
tm = time.time()
|
||||||
|
text = xf_aiui.question(self.q_msg)
|
||||||
|
util.log(1, '自然语言处理完成. 耗时: {} ms'.format(math.floor((time.time() - tm) * 1000)))
|
||||||
|
if text == '哎呀,你这么说我也不懂,详细点呗' or text == '':
|
||||||
|
util.log(1, '[!] 自然语言无语了!')
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": ""})
|
||||||
|
continue
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
util.log(1, '自然语言处理错误!')
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": ""})
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
text = answer
|
||||||
|
if len(user_name) == 0:
|
||||||
|
self.a_msg = text
|
||||||
|
else:
|
||||||
|
self.a_msg = user_name + ',' + text
|
||||||
|
|
||||||
|
elif index == 2:
|
||||||
|
self.a_msg = ['我们的直播间越来越多人咯', '感谢{}的到来'.format(user_name), '欢印{}来到我们的直播间'.format(user_name)][
|
||||||
|
random.randint(0, 2)]
|
||||||
|
|
||||||
|
elif index == 3:
|
||||||
|
msg = ""
|
||||||
|
for index in range(1, len(interact), 4):
|
||||||
|
try:
|
||||||
|
gift = interact[index + 2]
|
||||||
|
gift_name = '礼物'
|
||||||
|
if gift[0] != -1:
|
||||||
|
gift_name = gift[1]
|
||||||
|
msg = msg + "{}送给我的{}个{},".format(interact[index], interact[index + 3], gift_name)
|
||||||
|
except BaseException as e:
|
||||||
|
print("[System] 礼物处理错误!")
|
||||||
|
print(e)
|
||||||
|
self.a_msg = '感谢感谢,感谢' + msg
|
||||||
|
|
||||||
|
elif index == 4:
|
||||||
|
self.a_msg = '感谢关注'
|
||||||
|
|
||||||
|
self.last_speak_data = self.a_msg
|
||||||
|
self.speaking = True
|
||||||
|
MyThread(target=self.__say, args=['interact']).start()
|
||||||
|
else:
|
||||||
|
i = 0
|
||||||
|
self.interactive.clear()
|
||||||
|
config_items = config_util.config["items"]
|
||||||
|
items = []
|
||||||
|
for item in config_items:
|
||||||
|
if item["enabled"]:
|
||||||
|
items.append(item)
|
||||||
|
if len(items) > 0:
|
||||||
|
if self.item_index >= len(items):
|
||||||
|
self.item_index = 0
|
||||||
|
script_index = 0
|
||||||
|
item = items[self.item_index]
|
||||||
|
script_index = script_index + 1
|
||||||
|
explain_key = self.__get_explain_from_index(script_index)
|
||||||
|
if explain_key is None:
|
||||||
|
self.item_index = self.item_index + 1
|
||||||
|
script_index = 0
|
||||||
|
if self.item_index >= len(items):
|
||||||
|
self.item_index = 0
|
||||||
|
explain_key = self.__get_explain_from_index(script_index)
|
||||||
|
explain = item["explain"][explain_key]
|
||||||
|
if len(explain) > 0:
|
||||||
|
self.a_msg = explain
|
||||||
|
self.last_speak_data = self.a_msg
|
||||||
|
self.speaking = True
|
||||||
|
MyThread(target=self.__say, args=['script']).start()
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def __get_item_list(self) -> list:
|
||||||
|
items = []
|
||||||
|
for item in config_util.config["items"]:
|
||||||
|
if item["enabled"]:
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def __get_explain_from_index(self, index: int):
|
||||||
|
if index == 0:
|
||||||
|
return "character"
|
||||||
|
if index == 1:
|
||||||
|
return "discount"
|
||||||
|
if index == 2:
|
||||||
|
return "intro"
|
||||||
|
if index == 3:
|
||||||
|
return "price"
|
||||||
|
if index == 4:
|
||||||
|
return "promise"
|
||||||
|
if index == 5:
|
||||||
|
return "usage"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_interact(self, interact):
|
||||||
|
|
||||||
|
# 合并同类交互
|
||||||
|
# 进入
|
||||||
|
if interact[0] == 2:
|
||||||
|
itr = self.__get_interactive(2)
|
||||||
|
if itr is None:
|
||||||
|
self.interactive.append(interact)
|
||||||
|
else:
|
||||||
|
newItr = (2, itr[1] + ', ' + interact[1], itr[2])
|
||||||
|
self.interactive.remove(itr)
|
||||||
|
self.interactive.append(newItr)
|
||||||
|
|
||||||
|
# 送礼
|
||||||
|
elif interact[0] == 3:
|
||||||
|
itr = self.__get_interactive(3)
|
||||||
|
if itr is None:
|
||||||
|
self.interactive.append(interact)
|
||||||
|
else:
|
||||||
|
newItrList = []
|
||||||
|
newItrList.extend(itr)
|
||||||
|
newItrList.append(itr[2])
|
||||||
|
newItrList.append(itr[3])
|
||||||
|
newItrList.append(itr[4])
|
||||||
|
self.interactive.remove(itr)
|
||||||
|
self.interactive.append(tuple(newItrList))
|
||||||
|
|
||||||
|
# 关注
|
||||||
|
elif interact[0] == 4:
|
||||||
|
if self.__get_interactive(2) is None:
|
||||||
|
self.interactive.append(interact)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.interactive.append(interact)
|
||||||
|
MyThread(target=self.__update_mood, args=[interact[0]]).start()
|
||||||
|
MyThread(target=storer.storage_live_interact, args=[interact]).start()
|
||||||
|
|
||||||
|
def __get_interactive(self, interactType):
|
||||||
|
for interact in self.interactive:
|
||||||
|
if interact[0] == interactType:
|
||||||
|
return interact
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 适应模型计算
|
||||||
|
def __fay(self, index):
|
||||||
|
if 0 < index < 8:
|
||||||
|
self.X[0][index] += 1
|
||||||
|
# PRED = 1 /(1 + tf.exp(-tf.matmul(tf.constant(self.X,tf.float32), tf.constant(self.W,tf.float32))))
|
||||||
|
PRED = np.sum(self.X.reshape(-1) * self.W.reshape(-1))
|
||||||
|
if 0 < index < 8:
|
||||||
|
print('***PRED:{0}***'.format(PRED))
|
||||||
|
print(self.X.reshape(-1) * self.W.reshape(-1))
|
||||||
|
return PRED
|
||||||
|
|
||||||
|
# 发送情绪
|
||||||
|
def __send_mood(self):
|
||||||
|
while self.__running:
|
||||||
|
time.sleep(3)
|
||||||
|
if not self.sleep:
|
||||||
|
content = {'Topic': 'Unreal', 'Data': {'Key': 'mood', 'Value': self.mood}}
|
||||||
|
wsa_server.get_instance().add_cmd(content)
|
||||||
|
|
||||||
|
# 更新情绪
|
||||||
|
def __update_mood(self, typeIndex):
|
||||||
|
perception = config_util.config["interact"]["perception"]
|
||||||
|
if typeIndex == 1:
|
||||||
|
try:
|
||||||
|
result = xf_ltp.get_sentiment(self.q_msg)
|
||||||
|
chat_perception = perception["chat"]
|
||||||
|
if result == 2:
|
||||||
|
self.mood = self.mood + (chat_perception / 200.0)
|
||||||
|
elif result == 0:
|
||||||
|
self.mood = self.mood - (chat_perception / 100.0)
|
||||||
|
except BaseException as e:
|
||||||
|
print("[System] 情绪更新错误!")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
elif typeIndex == 2:
|
||||||
|
self.mood = self.mood + (perception["join"] / 100.0)
|
||||||
|
|
||||||
|
elif typeIndex == 3:
|
||||||
|
self.mood = self.mood + (perception["gift"] / 100.0)
|
||||||
|
|
||||||
|
elif typeIndex == 4:
|
||||||
|
self.mood = self.mood + (perception["follow"] / 100.0)
|
||||||
|
|
||||||
|
if self.mood >= 1:
|
||||||
|
self.mood = 1
|
||||||
|
if self.mood <= -1:
|
||||||
|
self.mood = -1
|
||||||
|
|
||||||
|
def __get_mood(self):
|
||||||
|
voice = tts_voice.get_voice_of(config_util.config["attribute"]["voice"])
|
||||||
|
if voice is None:
|
||||||
|
voice = EnumVoice.XIAO_XIAO
|
||||||
|
styleList = voice.value["styleList"]
|
||||||
|
sayType = styleList["calm"]
|
||||||
|
if -1 <= self.mood < -0.5:
|
||||||
|
sayType = styleList["angry"]
|
||||||
|
if -0.5 <= self.mood < -0.1:
|
||||||
|
sayType = styleList["lyrical"]
|
||||||
|
if -0.1 <= self.mood < 0.1:
|
||||||
|
sayType = styleList["calm"]
|
||||||
|
if 0.1 <= self.mood < 0.5:
|
||||||
|
sayType = styleList["assistant"]
|
||||||
|
if 0.5 <= self.mood <= 1:
|
||||||
|
sayType = styleList["cheerful"]
|
||||||
|
return sayType
|
||||||
|
|
||||||
|
# 合成声音,加上type代表是脚本还是互动
|
||||||
|
def __say(self, styleType):
|
||||||
|
try:
|
||||||
|
if len(self.a_msg) < 1:
|
||||||
|
self.speaking = False
|
||||||
|
else:
|
||||||
|
# print(self.__get_mood().name + self.a_msg)
|
||||||
|
util.printInfo(1, '菲菲', '({}) {}'.format(self.__get_mood(), self.a_msg))
|
||||||
|
MyThread(target=storer.storage_live_interact, args=[(0, '菲菲', self.a_msg)]).start()
|
||||||
|
util.log(1, '合成音频...')
|
||||||
|
tm = time.time()
|
||||||
|
result = self.sp.to_sample(self.a_msg, self.__get_mood())
|
||||||
|
util.log(1, '合成音频完成. 耗时: {} ms'.format(math.floor((time.time() - tm) * 1000)))
|
||||||
|
if result is not None:
|
||||||
|
# playsound(result)
|
||||||
|
# with wave.open(result, 'rb') as wav_file:
|
||||||
|
# wav_length = wav_file.getnframes() / float(wav_file.getframerate())
|
||||||
|
# time.sleep(wav_length)
|
||||||
|
MyThread(target=self.__send_audio, args=[result, styleType]).start()
|
||||||
|
# MyThread(target=self.__play_audio, args=[result]).start()
|
||||||
|
# MyThread(target=self.__waiting_speaking, args=[result]).start()
|
||||||
|
return result
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
# print("tts失败!!!!!!!!!!!!!")
|
||||||
|
self.speaking = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __play_sound(self, file_url):
|
||||||
|
util.log(1, '播放音频...')
|
||||||
|
util.log(1, '问答处理总时长:{} ms'.format(math.floor((time.time() - self.last_quest_time) * 1000)))
|
||||||
|
pygame.mixer.music.load(file_url)
|
||||||
|
pygame.mixer.music.play()
|
||||||
|
|
||||||
|
def __send_audio(self, file_url, say_type):
|
||||||
|
try:
|
||||||
|
audio_length = eyed3.load(file_url).info.time_secs
|
||||||
|
if audio_length <= config_util.config["interact"]["maxInteractTime"] or say_type == "script":
|
||||||
|
if config_util.config["interact"]["playSound"]:
|
||||||
|
self.__play_sound(file_url)
|
||||||
|
else:
|
||||||
|
content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'Time': audio_length, 'Type': say_type}}
|
||||||
|
wsa_server.get_instance().add_cmd(content)
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": self.a_msg})
|
||||||
|
time.sleep(audio_length + 0.5)
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": ""})
|
||||||
|
if config_util.config["interact"]["playSound"]:
|
||||||
|
util.log(1, '结束播放!')
|
||||||
|
self.speaking = False
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# def __send_audio(self, file_url, say_type):
|
||||||
|
# try:
|
||||||
|
# # time.sleep(0.25)
|
||||||
|
# with wave.open(file_url, 'rb') as wav_file:
|
||||||
|
# wav_length = wav_file.getnframes() / float(wav_file.getframerate())
|
||||||
|
# print(wav_length)
|
||||||
|
# if wav_length <= config_util.config["interact"]["maxInteractTime"] or say_type == "script":
|
||||||
|
# if config_util.config["interact"]["playSound"]:
|
||||||
|
# self.__play_sound(file_url)
|
||||||
|
# else:
|
||||||
|
# content = {'Topic': 'Unreal', 'Data': {'Key': 'audio', 'Value': os.path.abspath(file_url), 'Time': wav_length, 'Type': say_type}}
|
||||||
|
# wsa_server.get_instance().add_cmd(content)
|
||||||
|
# time.sleep(wav_length + 0.5)
|
||||||
|
# self.speaking = False
|
||||||
|
# except Exception as e:
|
||||||
|
# print(e)
|
||||||
|
|
||||||
|
def __waiting_speaking(self, file_url):
|
||||||
|
try:
|
||||||
|
time.sleep(5)
|
||||||
|
print('[' + str(int(time.time())) + '][菲菲] [S] [开始发言]')
|
||||||
|
with wave.open(file_url, 'rb') as wav_file:
|
||||||
|
wav_length = wav_file.getnframes() / float(wav_file.getframerate())
|
||||||
|
time.sleep(wav_length)
|
||||||
|
self.last_interact_time = time.time()
|
||||||
|
self.speaking = False
|
||||||
|
print('[' + str(int(time.time())) + '][菲菲] [E] [结束发言]')
|
||||||
|
time.sleep(30)
|
||||||
|
os.remove(file_url)
|
||||||
|
except:
|
||||||
|
self.last_interact_time = time.time()
|
||||||
|
self.speaking = False
|
||||||
|
|
||||||
|
# 冷场情绪更新
|
||||||
|
def __update_mood_runnable(self):
|
||||||
|
while self.__running:
|
||||||
|
time.sleep(10)
|
||||||
|
update = config_util.config["interact"]["perception"]["indifferent"] / 100
|
||||||
|
if len(self.interactive) < 1:
|
||||||
|
if self.mood > 0:
|
||||||
|
if self.mood > update:
|
||||||
|
self.mood = self.mood - update
|
||||||
|
else:
|
||||||
|
self.mood = 0
|
||||||
|
elif self.mood < 0:
|
||||||
|
if self.mood < -update:
|
||||||
|
self.mood = self.mood + update
|
||||||
|
else:
|
||||||
|
self.mood = 0
|
||||||
|
|
||||||
|
def set_sleep(self, sleep):
|
||||||
|
self.sleep = sleep
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
MyThread(target=self.__send_mood).start()
|
||||||
|
MyThread(target=self.__auto_speak).start()
|
||||||
|
MyThread(target=self.__update_mood_runnable).start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.__running = False
|
||||||
|
self.sp.close()
|
163
core/recorder.py
Normal file
163
core/recorder.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import audioop
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
import pyaudio
|
||||||
|
|
||||||
|
from ai_module.ali_nls import ALiNls
|
||||||
|
from core import wsa_server
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import util
|
||||||
|
|
||||||
|
# 启动时间 (秒)
|
||||||
|
_ATTACK = 0.2
|
||||||
|
|
||||||
|
# 释放时间 (秒)
|
||||||
|
_RELEASE = 0.75
|
||||||
|
|
||||||
|
|
||||||
|
class Recorder:
|
||||||
|
|
||||||
|
def __init__(self, device, fay):
|
||||||
|
self.__device = device
|
||||||
|
self.__fay = fay
|
||||||
|
|
||||||
|
self.__RATE = 16000
|
||||||
|
self.__FORMAT = pyaudio.paInt16
|
||||||
|
self.__CHANNELS = 1
|
||||||
|
|
||||||
|
self.__running = True
|
||||||
|
self.__processing = False
|
||||||
|
self.__history_level = []
|
||||||
|
self.__history_data = []
|
||||||
|
self.__dynamic_threshold = 0.5
|
||||||
|
|
||||||
|
self.__MAX_LEVEL = 25000
|
||||||
|
self.__MAX_BLOCK = 100
|
||||||
|
|
||||||
|
self.__aLiNls = ALiNls()
|
||||||
|
|
||||||
|
def __findInternalRecordingDevice(self, p):
|
||||||
|
for i in range(p.get_device_count()):
|
||||||
|
devInfo = p.get_device_info_by_index(i)
|
||||||
|
if devInfo['name'].find(self.__device) >= 0 and devInfo['hostApi'] == 0:
|
||||||
|
return i
|
||||||
|
util.log(1, '[!] 无法找到内录设备!')
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def __get_history_average(self, number):
|
||||||
|
total = 0
|
||||||
|
num = 0
|
||||||
|
for i in range(len(self.__history_level) - 1, -1, -1):
|
||||||
|
level = self.__history_level[i]
|
||||||
|
total += level
|
||||||
|
num += 1
|
||||||
|
if num >= number:
|
||||||
|
break
|
||||||
|
return total / num
|
||||||
|
|
||||||
|
def __get_history_percentage(self, number):
|
||||||
|
return (self.__get_history_average(number) / self.__MAX_LEVEL) * 1.05 + 0.02
|
||||||
|
|
||||||
|
def __print_level(self, level):
|
||||||
|
text = ""
|
||||||
|
per = level / self.__MAX_LEVEL
|
||||||
|
if per > 1:
|
||||||
|
per = 1
|
||||||
|
bs = int(per * self.__MAX_BLOCK)
|
||||||
|
for i in range(bs):
|
||||||
|
text += "#"
|
||||||
|
for i in range(self.__MAX_BLOCK - bs):
|
||||||
|
text += "-"
|
||||||
|
print(text + " [" + str(int(per * 100)) + "%]")
|
||||||
|
|
||||||
|
def __waitingResult(self, iat: ALiNls):
|
||||||
|
self.processing = True
|
||||||
|
t = time.time()
|
||||||
|
tm = time.time()
|
||||||
|
# 等待结果返回
|
||||||
|
while not iat.done and time.time() - t < 1:
|
||||||
|
time.sleep(0.01)
|
||||||
|
text = iat.finalResults
|
||||||
|
util.log(1, "语音处理完成! 耗时: {} ms".format(math.floor((time.time() - tm) * 1000)))
|
||||||
|
if len(text) > 0:
|
||||||
|
self.on_speaking(text)
|
||||||
|
self.processing = False
|
||||||
|
else:
|
||||||
|
util.log(1, "[!] 语音未检测到内容!")
|
||||||
|
self.processing = False
|
||||||
|
self.dynamic_threshold = self.__get_history_percentage(30)
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": ""})
|
||||||
|
|
||||||
|
def __record(self):
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
device_id = self.__findInternalRecordingDevice(p)
|
||||||
|
if device_id < 0:
|
||||||
|
return
|
||||||
|
stream = p.open(input_device_index=device_id, rate=self.__RATE, format=self.__FORMAT, channels=self.__CHANNELS, input=True)
|
||||||
|
|
||||||
|
isSpeaking = False
|
||||||
|
last_mute_time = time.time()
|
||||||
|
last_speaking_time = time.time()
|
||||||
|
while self.__running:
|
||||||
|
data = stream.read(1024)
|
||||||
|
level = audioop.rms(data, 2)
|
||||||
|
if len(self.__history_data) >= 5:
|
||||||
|
self.__history_data.pop(0)
|
||||||
|
if len(self.__history_level) >= 500:
|
||||||
|
self.__history_level.pop(0)
|
||||||
|
self.__history_data.append(data)
|
||||||
|
self.__history_level.append(level)
|
||||||
|
|
||||||
|
percentage = level / self.__MAX_LEVEL
|
||||||
|
history_percentage = self.__get_history_percentage(30)
|
||||||
|
|
||||||
|
if history_percentage > self.__dynamic_threshold:
|
||||||
|
self.__dynamic_threshold += (history_percentage - self.__dynamic_threshold) * 0.0025
|
||||||
|
elif history_percentage < self.__dynamic_threshold:
|
||||||
|
self.__dynamic_threshold += (history_percentage - self.__dynamic_threshold) * 1
|
||||||
|
|
||||||
|
soon = False
|
||||||
|
if percentage > self.__dynamic_threshold and not self.__fay.speaking:
|
||||||
|
last_speaking_time = time.time()
|
||||||
|
if not self.__processing and not isSpeaking and time.time() - last_mute_time > _ATTACK:
|
||||||
|
soon = True
|
||||||
|
isSpeaking = True
|
||||||
|
util.log(3, "聆听中...")
|
||||||
|
self.__aLiNls = ALiNls()
|
||||||
|
try:
|
||||||
|
self.__aLiNls.start()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
for buf in self.__history_data:
|
||||||
|
self.__aLiNls.send(buf)
|
||||||
|
else:
|
||||||
|
last_mute_time = time.time()
|
||||||
|
if isSpeaking:
|
||||||
|
if time.time() - last_speaking_time > _RELEASE:
|
||||||
|
isSpeaking = False
|
||||||
|
self.__aLiNls.end()
|
||||||
|
util.log(1, "语音处理中...")
|
||||||
|
self.__fay.last_quest_time = time.time()
|
||||||
|
self.__waitingResult(self.__aLiNls)
|
||||||
|
if not soon and isSpeaking:
|
||||||
|
self.__aLiNls.send(data)
|
||||||
|
|
||||||
|
stream.stop_stream()
|
||||||
|
stream.close()
|
||||||
|
p.terminate()
|
||||||
|
|
||||||
|
def set_processing(self, processing):
|
||||||
|
self.__processing = processing
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
MyThread(target=self.__record).start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.__running = False
|
||||||
|
self.__aLiNls.end()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_speaking(self, text):
|
||||||
|
pass
|
37
core/tts_voice.py
Normal file
37
core/tts_voice.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class EnumVoice(Enum):
|
||||||
|
XIAO_XIAO = {
|
||||||
|
"name": "晓晓",
|
||||||
|
"voiceName": "zh-CN-XiaoxiaoNeural",
|
||||||
|
"styleList": {
|
||||||
|
"angry": "angry",
|
||||||
|
"lyrical": "lyrical",
|
||||||
|
"calm": "gentle",
|
||||||
|
"assistant": "affectionate",
|
||||||
|
"cheerful": "cheerful"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
YUN_XI = {
|
||||||
|
"name": "云溪",
|
||||||
|
"voiceName": "zh-CN-YunxiNeural",
|
||||||
|
"styleList": {
|
||||||
|
"angry": "angry",
|
||||||
|
"lyrical": "disgruntled",
|
||||||
|
"calm": "calm",
|
||||||
|
"assistant": "assistant",
|
||||||
|
"cheerful": "cheerful"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_voice_list():
|
||||||
|
return [EnumVoice.YUN_XI, EnumVoice.XIAO_XIAO]
|
||||||
|
|
||||||
|
|
||||||
|
def get_voice_of(name):
|
||||||
|
for voice in get_voice_list():
|
||||||
|
if voice.name == name:
|
||||||
|
return voice
|
||||||
|
return None
|
290
core/viewer.py
Normal file
290
core/viewer.py
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support.expected_conditions import presence_of_element_located
|
||||||
|
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import config_util, util
|
||||||
|
|
||||||
|
USER_URL = 'https://www.douyin.com/user/'
|
||||||
|
|
||||||
|
|
||||||
|
class Viewer:
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
self.GIFT_TYPES = {
|
||||||
|
'0ea40b8376ef8157791b928a339ed9c9': (1, '小星星', 1),
|
||||||
|
'a29d6cdc0abb7286fdd403915196eaa7': (2, '玫瑰', 1),
|
||||||
|
'802a21ae29f9fae5abe3693de9f874bd': (3, '抖音', 1),
|
||||||
|
'a24b3cc863742fd4bc3de0f53dac4487': (4, '大啤酒', 2),
|
||||||
|
'4960c39f645d524beda5d50dc372510e': (5, '你最好看', 2),
|
||||||
|
'e9b7db267d0501b8963d8000c091e123': (6, '人气票', 1),
|
||||||
|
'698373dfdac86a90b54facdc38698cbc': (7, '粉丝团灯牌', 1)
|
||||||
|
}
|
||||||
|
self.__running = True
|
||||||
|
self.live_driver = None
|
||||||
|
self.user_driver = None
|
||||||
|
self.user_sec_uid = None
|
||||||
|
self.last_join_data = ''
|
||||||
|
self.last_interact_datas = []
|
||||||
|
self.live_started = False
|
||||||
|
self.last_chat_item_index = 0
|
||||||
|
|
||||||
|
def __start(self):
|
||||||
|
MyThread(target=self.__driver_alive_runnable).start()
|
||||||
|
self.chrome_options = Options()
|
||||||
|
self.chrome_options.add_argument('--headless')
|
||||||
|
self.chrome_options.add_argument('--blink-settings=imagesEnabled=false')
|
||||||
|
self.live_driver = webdriver.Chrome(config_util.system_chrome_driver, options=self.chrome_options)
|
||||||
|
self.live_driver.get(self.url)
|
||||||
|
self.user_driver = webdriver.Chrome(config_util.system_chrome_driver, options=self.chrome_options)
|
||||||
|
self.__wait_live_start()
|
||||||
|
self.user_sec_uid = self.__get_render_data(self.live_driver)['initialState']['roomStore']['roomInfo']['room']['owner']['sec_uid']
|
||||||
|
MyThread(target=self.__live_state_runnable).start()
|
||||||
|
MyThread(target=self.__join_runnable).start()
|
||||||
|
MyThread(target=self.__interact_runnable).start()
|
||||||
|
MyThread(target=self.__follower_runnable).start()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
MyThread(target=self.__start).start()
|
||||||
|
|
||||||
|
def is_live_started(self):
|
||||||
|
return self.live_started
|
||||||
|
|
||||||
|
def __wait_live_start(self):
|
||||||
|
if self.__is_live():
|
||||||
|
return
|
||||||
|
util.log(1, '等待直播开始...')
|
||||||
|
time.sleep(30)
|
||||||
|
while not self.__is_live() and self.__running:
|
||||||
|
try:
|
||||||
|
self.live_driver.get(self.url)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
def __is_live(self):
|
||||||
|
try:
|
||||||
|
xpath = '//*[@id="_douyin_live_scroll_container_"]/div/div[2]/div/div[2]/div/div[2]/div'
|
||||||
|
element = self.live_driver.find_element_by_xpath(xpath)
|
||||||
|
return '结束' not in element.text
|
||||||
|
except BaseException as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __driver_alive_runnable(self):
|
||||||
|
while self.__running:
|
||||||
|
time.sleep(0.1)
|
||||||
|
try:
|
||||||
|
if self.live_driver is not None:
|
||||||
|
try:
|
||||||
|
self.live_driver.execute_script('javascript:void(0);')
|
||||||
|
except:
|
||||||
|
if self.__running:
|
||||||
|
self.live_driver = webdriver.Chrome(config_util.system_chrome_driver, options=self.chrome_options)
|
||||||
|
self.live_driver.get(self.url)
|
||||||
|
if self.user_driver is not None:
|
||||||
|
try:
|
||||||
|
self.user_driver.execute_script('javascript:void(0);')
|
||||||
|
except:
|
||||||
|
if self.__running:
|
||||||
|
self.user_driver = webdriver.Chrome(config_util.system_chrome_driver, options=self.chrome_options)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __live_state_runnable(self):
|
||||||
|
while self.__running:
|
||||||
|
is_live = self.__is_live()
|
||||||
|
if is_live != self.live_started:
|
||||||
|
self.live_started = self.__is_live()
|
||||||
|
self.on_change_state(is_live)
|
||||||
|
if not is_live:
|
||||||
|
util.log(1, '直播直播已结束,等待下场直播开始...')
|
||||||
|
if is_live != True:
|
||||||
|
try:
|
||||||
|
self.live_driver.get(self.url)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
def __get_render_data(self, driver):
|
||||||
|
wait = WebDriverWait(driver, 10)
|
||||||
|
first_result = wait.until(presence_of_element_located((By.ID, "RENDER_DATA")))
|
||||||
|
return json.loads(requests.utils.unquote(first_result.get_attribute("textContent")))
|
||||||
|
|
||||||
|
def __get_interact_type(self, text):
|
||||||
|
ary = text.split(':')
|
||||||
|
if len(ary) >= 2:
|
||||||
|
content_ary = ary[1].split(' ')
|
||||||
|
if len(content_ary) == 3 and content_ary[0] == '送出了':
|
||||||
|
return 3
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def __get_gift_type(self, url):
|
||||||
|
for gift_id in self.GIFT_TYPES.keys():
|
||||||
|
if gift_id in url:
|
||||||
|
return self.GIFT_TYPES.get(gift_id)
|
||||||
|
return -1, '其他礼物', 0
|
||||||
|
|
||||||
|
def __get_join_data(self):
|
||||||
|
try:
|
||||||
|
xpath = '//*[@id="_douyin_live_scroll_container_"]/div/div[2]/div/div[2]/div/div[1]/div/div/div/div[1]/div/div[2]'
|
||||||
|
element = self.live_driver.find_element_by_xpath(xpath)
|
||||||
|
ary = element.text.split('\n')
|
||||||
|
text = ary[len(ary) - 1]
|
||||||
|
if len(text) > 0 and self.last_join_data != text:
|
||||||
|
self.last_join_data = text
|
||||||
|
user = text[0:len(text) - 3]
|
||||||
|
return 2, user, '来了'
|
||||||
|
except BaseException as e:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __get_interact_data(self):
|
||||||
|
interact_data = []
|
||||||
|
chatroom_xpath = '//*[@id="_douyin_live_scroll_container_"]/div/div[2]/div/div[2]/div/div[1]/div/div/div/div[1]/div/div[1]'
|
||||||
|
try:
|
||||||
|
chatroom_element = self.live_driver.find_element_by_xpath(chatroom_xpath)
|
||||||
|
|
||||||
|
index_range = None
|
||||||
|
|
||||||
|
if self.last_chat_item_index < 100:
|
||||||
|
start = self.last_chat_item_index + 1
|
||||||
|
if start < 1:
|
||||||
|
start = 1
|
||||||
|
index_range = range(start, 101) # 升序
|
||||||
|
else:
|
||||||
|
index_range = range(100, 0, -1) # 降序
|
||||||
|
|
||||||
|
# print("\n上一次: {}".format(self.last_chat_item_index))
|
||||||
|
for index in index_range:
|
||||||
|
|
||||||
|
# print("到了: {}".format(index))
|
||||||
|
chatroom_item = None
|
||||||
|
try:
|
||||||
|
chatroom_item = chatroom_element.find_element_by_xpath(chatroom_xpath + '/div[' + str(index) + ']')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
item_id = None
|
||||||
|
if self.last_chat_item_index < 100:
|
||||||
|
if chatroom_item is None:
|
||||||
|
self.last_chat_item_index = index - 1
|
||||||
|
break
|
||||||
|
elif index >= 100:
|
||||||
|
self.last_chat_item_index = index
|
||||||
|
else:
|
||||||
|
if chatroom_item is None:
|
||||||
|
continue
|
||||||
|
item_id = chatroom_item.id
|
||||||
|
if item_id in self.last_interact_datas:
|
||||||
|
break
|
||||||
|
|
||||||
|
# print(index)
|
||||||
|
|
||||||
|
if len(self.last_interact_datas) > 200:
|
||||||
|
self.last_interact_datas.pop(0)
|
||||||
|
|
||||||
|
self.last_interact_datas.append(item_id)
|
||||||
|
item_text = chatroom_item.text
|
||||||
|
ary = chatroom_item.text.replace('\r', '').split('\n')
|
||||||
|
text = ary[len(ary) - 1]
|
||||||
|
if len(text) < 1 and len(ary) > 1:
|
||||||
|
text = ary[len(ary) - 2]
|
||||||
|
speak = self.__get_speak(text)
|
||||||
|
if speak is None:
|
||||||
|
# print("无法分析[O]: " + item_text)
|
||||||
|
# print("无法分析[R]: " + text)
|
||||||
|
continue
|
||||||
|
if self.__get_interact_type(text) == 3:
|
||||||
|
item_msg = None
|
||||||
|
try:
|
||||||
|
item_msg = chatroom_element.find_element_by_xpath(
|
||||||
|
chatroom_xpath + '/div[' + str(index) + ']/div/span[3]/span/span/img')
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
gift = self.__get_gift_type(item_msg.get_attribute('src'))
|
||||||
|
arg = speak[1].split(' ')
|
||||||
|
amount = int(arg[len(arg) - 1]) # 礼物数量
|
||||||
|
interact_data.append((3, speak[0], ('送出了 {0} X {1}'.format(gift[1], amount)), gift, amount))
|
||||||
|
else:
|
||||||
|
interact_data.append((1, speak[0], speak[1]))
|
||||||
|
except BaseException as e:
|
||||||
|
interact_data.reverse()
|
||||||
|
return interact_data
|
||||||
|
interact_data.reverse()
|
||||||
|
return interact_data
|
||||||
|
|
||||||
|
def __get_speak(self, text):
|
||||||
|
ary = text.split(':')
|
||||||
|
if len(ary) < 2:
|
||||||
|
return None
|
||||||
|
user = ary[0]
|
||||||
|
speak = text[len(ary[0]) + 1:]
|
||||||
|
if len(user) > 0 and len(speak) > 0:
|
||||||
|
return user, speak
|
||||||
|
|
||||||
|
def __join_runnable(self):
|
||||||
|
while self.__running:
|
||||||
|
if not self.live_started:
|
||||||
|
continue
|
||||||
|
# 进入 抓取
|
||||||
|
join_data = self.__get_join_data()
|
||||||
|
if join_data is not None:
|
||||||
|
self.on_interact(join_data, time.time())
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
def __interact_runnable(self):
|
||||||
|
while self.__running:
|
||||||
|
if not self.live_started:
|
||||||
|
continue
|
||||||
|
# 发言 & 刷礼物 抓取
|
||||||
|
for interact in self.__get_interact_data():
|
||||||
|
MyThread(target=self.on_interact, args=[interact, time.time()]).start()
|
||||||
|
# self.on_interact(interact, time.time())
|
||||||
|
|
||||||
|
def __follower_runnable(self):
|
||||||
|
followers = -1
|
||||||
|
while self.__running:
|
||||||
|
# 关注 抓取
|
||||||
|
try:
|
||||||
|
time.sleep(1.0 + random.random())
|
||||||
|
self.user_driver.get(USER_URL + self.user_sec_uid)
|
||||||
|
time.sleep(0.2)
|
||||||
|
render_data = self.__get_render_data(self.user_driver)
|
||||||
|
fs = -1
|
||||||
|
for i in range(100, -1, -1):
|
||||||
|
if str(i) in render_data and 'user' in render_data[str(i)] and 'user' in render_data[str(i)]['user'] and 'followerCount' in render_data[str(i)]['user']['user']:
|
||||||
|
fs = int(render_data[str(i)]['user']['user']['followerCount'])
|
||||||
|
break
|
||||||
|
if fs >= 0:
|
||||||
|
if self.live_started and 0 < followers < fs:
|
||||||
|
self.on_interact((4, 'None', '粉丝关注'), time.time())
|
||||||
|
followers = fs
|
||||||
|
else:
|
||||||
|
util.log(1, '粉丝数获取异常')
|
||||||
|
except BaseException as e:
|
||||||
|
util.log(1, e)
|
||||||
|
util.log(1, '粉丝数获取异常')
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.__running = False
|
||||||
|
if self.live_driver:
|
||||||
|
self.live_driver.quit()
|
||||||
|
if self.user_driver:
|
||||||
|
self.user_driver.quit()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_interact(self, interact, event_time):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_change_state(self, is_live_started):
|
||||||
|
pass
|
123
core/wsa_server.py
Normal file
123
core/wsa_server.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
from asyncio import AbstractEventLoop
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from websockets.legacy.server import Serve
|
||||||
|
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
|
||||||
|
|
||||||
|
class MyServer:
|
||||||
|
def __init__(self, host='127.0.0.1', port=10000):
|
||||||
|
self.__host = host # ip
|
||||||
|
self.__port = port # 端口号
|
||||||
|
self.__listCmd = [] # 要发送的信息的列表
|
||||||
|
self.__server: Serve = None
|
||||||
|
self.__message_value = None # client返回消息的value
|
||||||
|
self.__event_loop: AbstractEventLoop = None
|
||||||
|
self.__running = True
|
||||||
|
self.__pending = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.stop_server()
|
||||||
|
|
||||||
|
async def __consumer_handler(self, websocket, path):
|
||||||
|
async for message in websocket:
|
||||||
|
await self.__consumer(message)
|
||||||
|
|
||||||
|
async def __producer_handler(self, websocket, path):
|
||||||
|
while self.__running:
|
||||||
|
await asyncio.sleep(0.000001)
|
||||||
|
message = await self.__producer()
|
||||||
|
if message:
|
||||||
|
await websocket.send(message)
|
||||||
|
# util.log('发送 {}'.format(message))
|
||||||
|
|
||||||
|
async def __handler(self, websocket, path):
|
||||||
|
consumer_task = asyncio.ensure_future(self.__consumer_handler(websocket, path))
|
||||||
|
producer_task = asyncio.ensure_future(self.__producer_handler(websocket, path))
|
||||||
|
done, self.__pending = await asyncio.wait([consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED, )
|
||||||
|
for task in self.__pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
# 接收处理
|
||||||
|
async def __consumer(self, message):
|
||||||
|
pass
|
||||||
|
# print('recv message: {0}'.format(message))
|
||||||
|
|
||||||
|
# 发送处理
|
||||||
|
async def __producer(self):
|
||||||
|
if len(self.__listCmd) > 0:
|
||||||
|
return self.__listCmd.pop(0)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 创建server
|
||||||
|
def __connect(self):
|
||||||
|
self.__event_loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(self.__event_loop)
|
||||||
|
self.__isExecute = True
|
||||||
|
if self.__server:
|
||||||
|
print('server already exist')
|
||||||
|
return
|
||||||
|
self.__server = websockets.serve(self.__handler, self.__host, self.__port)
|
||||||
|
asyncio.get_event_loop().run_until_complete(self.__server)
|
||||||
|
asyncio.get_event_loop().run_forever()
|
||||||
|
|
||||||
|
# 往要发送的命令列表中,添加命令
|
||||||
|
def add_cmd(self, content):
|
||||||
|
if not self.__running:
|
||||||
|
return
|
||||||
|
jsonObj = json.dumps(content)
|
||||||
|
self.__listCmd.append(jsonObj)
|
||||||
|
# util.log('命令 {}'.format(content))
|
||||||
|
|
||||||
|
# 开启服务
|
||||||
|
def start_server(self):
|
||||||
|
MyThread(target=self.__connect).start()
|
||||||
|
|
||||||
|
# 关闭服务
|
||||||
|
def stop_server(self):
|
||||||
|
self.__running = False
|
||||||
|
if self.__server is None:
|
||||||
|
return
|
||||||
|
self.__server.ws_server.close()
|
||||||
|
self.__server = None
|
||||||
|
try:
|
||||||
|
all_tasks = asyncio.all_tasks(self.__event_loop)
|
||||||
|
for task in all_tasks:
|
||||||
|
# print(task.cancel())
|
||||||
|
while not task.cancel():
|
||||||
|
print("无法关闭!")
|
||||||
|
self.__event_loop.stop()
|
||||||
|
self.__event_loop.close()
|
||||||
|
except BaseException as e:
|
||||||
|
print("Error: {}".format(e))
|
||||||
|
|
||||||
|
|
||||||
|
__instance: MyServer = None
|
||||||
|
__web_instance: MyServer = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_instance(host='127.0.0.1', port=10000) -> MyServer:
|
||||||
|
global __instance
|
||||||
|
if __instance is None:
|
||||||
|
__instance = MyServer(host, port)
|
||||||
|
return __instance
|
||||||
|
|
||||||
|
|
||||||
|
def new_web_instance(host='127.0.0.1', port=10000) -> MyServer:
|
||||||
|
global __web_instance
|
||||||
|
if __web_instance is None:
|
||||||
|
__web_instance = MyServer(host, port)
|
||||||
|
return __web_instance
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance() -> MyServer:
|
||||||
|
return __instance
|
||||||
|
|
||||||
|
|
||||||
|
def get_web_instance() -> MyServer:
|
||||||
|
return __web_instance
|
170
fay_booter.py
Normal file
170
fay_booter.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from core.recorder import Recorder
|
||||||
|
from core.fay_core import FeiFei
|
||||||
|
from core.viewer import Viewer
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import util, config_util
|
||||||
|
|
||||||
|
feiFei: FeiFei = None
|
||||||
|
viewerListener: Viewer = None
|
||||||
|
recorderListener: Recorder = None
|
||||||
|
|
||||||
|
__running = True
|
||||||
|
|
||||||
|
|
||||||
|
class ViewerListener(Viewer):
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
super().__init__(url)
|
||||||
|
|
||||||
|
def on_interact(self, interact, event_time):
|
||||||
|
type_names = {
|
||||||
|
1: '发言',
|
||||||
|
2: '进入',
|
||||||
|
3: '送礼',
|
||||||
|
4: '关注'
|
||||||
|
}
|
||||||
|
util.printInfo(1, type_names[interact[0]], '{}: {}'.format(interact[1], interact[2]), event_time)
|
||||||
|
if interact[0] == 1:
|
||||||
|
feiFei.last_quest_time = time.time()
|
||||||
|
thr = MyThread(target=feiFei.on_interact, args=[interact])
|
||||||
|
thr.start()
|
||||||
|
thr.join()
|
||||||
|
|
||||||
|
def on_change_state(self, is_live_started):
|
||||||
|
feiFei.set_sleep(not is_live_started)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RecorderListener(Recorder):
|
||||||
|
|
||||||
|
def __init__(self, device, fei):
|
||||||
|
super().__init__(device, fei)
|
||||||
|
|
||||||
|
def on_speaking(self, text):
|
||||||
|
interact = (1, '', text)
|
||||||
|
util.printInfo(3, "语音", '{}'.format(interact[2]), time.time())
|
||||||
|
feiFei.on_interact(interact)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def console_listener():
|
||||||
|
type_names = {
|
||||||
|
1: '发言',
|
||||||
|
2: '进入',
|
||||||
|
3: '送礼',
|
||||||
|
4: '关注'
|
||||||
|
}
|
||||||
|
while __running:
|
||||||
|
text = input()
|
||||||
|
args = text.split(' ')
|
||||||
|
|
||||||
|
if len(args) == 0 or len(args[0]) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if args[0] == 'help':
|
||||||
|
util.log(1, 'in <msg> \t通过控制台交互')
|
||||||
|
util.log(1, 'restart \t重启服务')
|
||||||
|
util.log(1, 'stop \t\t关闭服务')
|
||||||
|
|
||||||
|
elif args[0] == 'stop':
|
||||||
|
stop()
|
||||||
|
break
|
||||||
|
|
||||||
|
elif args[0] == 'restart':
|
||||||
|
stop()
|
||||||
|
time.sleep(0.1)
|
||||||
|
start()
|
||||||
|
|
||||||
|
elif args[0] == 'in':
|
||||||
|
if len(args) == 1:
|
||||||
|
util.log(1, '错误的参数!')
|
||||||
|
msg = text[3:len(text)]
|
||||||
|
i = 1
|
||||||
|
try:
|
||||||
|
i = int(msg)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if i < 1:
|
||||||
|
i = 1
|
||||||
|
if i > 4:
|
||||||
|
i = 4
|
||||||
|
util.printInfo(1, type_names[i], '{}: {}'.format('控制台', msg))
|
||||||
|
if i == 1:
|
||||||
|
feiFei.last_quest_time = time.time()
|
||||||
|
thr = MyThread(target=feiFei.on_interact, args=[(i, '', msg)])
|
||||||
|
thr.start()
|
||||||
|
thr.join()
|
||||||
|
|
||||||
|
else:
|
||||||
|
util.log(1, '未知命令!使用 \'help\' 获取帮助.')
|
||||||
|
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
global feiFei
|
||||||
|
global viewerListener
|
||||||
|
global recorderListener
|
||||||
|
global __running
|
||||||
|
|
||||||
|
util.log(1, '正在关闭服务...')
|
||||||
|
__running = False
|
||||||
|
# util.log('正在关闭通讯服务...')
|
||||||
|
# wsa_server.get_instance().stop_server()
|
||||||
|
if viewerListener is not None:
|
||||||
|
util.log(1, '正在关闭直播服务...')
|
||||||
|
viewerListener.stop()
|
||||||
|
if recorderListener is not None:
|
||||||
|
util.log(1, '正在关闭录音服务...')
|
||||||
|
recorderListener.stop()
|
||||||
|
util.log(1, '正在关闭核心服务...')
|
||||||
|
feiFei.stop()
|
||||||
|
util.log(1, '服务已关闭!')
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
# global ws_server
|
||||||
|
global feiFei
|
||||||
|
global viewerListener
|
||||||
|
global recorderListener
|
||||||
|
global __running
|
||||||
|
|
||||||
|
util.log(1, '开启服务...')
|
||||||
|
__running = True
|
||||||
|
util.log(1, '读取配置...')
|
||||||
|
config_util.load_config()
|
||||||
|
#
|
||||||
|
# util.log('开启通讯服务...')
|
||||||
|
# ws_server = MyServer()
|
||||||
|
# ws_server.start_server()
|
||||||
|
|
||||||
|
util.log(1, '开启核心服务...')
|
||||||
|
feiFei = FeiFei()
|
||||||
|
feiFei.start()
|
||||||
|
|
||||||
|
liveRoom = config_util.config['source']['liveRoom']
|
||||||
|
record = config_util.config['source']['record']
|
||||||
|
|
||||||
|
if liveRoom['enabled']:
|
||||||
|
util.log(1, '开启直播服务...')
|
||||||
|
viewerListener = ViewerListener(liveRoom['url']) # 监听直播间
|
||||||
|
viewerListener.start()
|
||||||
|
|
||||||
|
if record['enabled']:
|
||||||
|
util.log(1, '开启录音服务...')
|
||||||
|
recorderListener = RecorderListener(record['device'], feiFei) # 监听麦克风
|
||||||
|
recorderListener.start()
|
||||||
|
|
||||||
|
util.log(1, '注册命令...')
|
||||||
|
MyThread(target=console_listener).start() # 监听控制台
|
||||||
|
|
||||||
|
util.log(1, '完成!')
|
||||||
|
util.log(1, '使用 \'help\' 获取帮助.')
|
||||||
|
|
||||||
|
# if __name__ == '__main__':
|
||||||
|
# ws_server: MyServer = None
|
||||||
|
# feiFei: FeiFei = None
|
||||||
|
# viewerListener: Viewer = None
|
||||||
|
# recorderListener: Recorder = None
|
||||||
|
# start()
|
||||||
|
# config_util.save_config()
|
82
gui/flask_server.py
Normal file
82
gui/flask_server.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pyaudio
|
||||||
|
from flask import Flask, render_template, request
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
import fay_booter
|
||||||
|
from core import wsa_server
|
||||||
|
from core.tts_voice import EnumVoice
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
from utils import config_util
|
||||||
|
|
||||||
|
__app = Flask(__name__)
|
||||||
|
CORS(__app, supports_credentials=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_template():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
def __get_device_list():
|
||||||
|
audio = pyaudio.PyAudio()
|
||||||
|
device_list = []
|
||||||
|
for i in range(audio.get_device_count()):
|
||||||
|
devInfo = audio.get_device_info_by_index(i)
|
||||||
|
if devInfo['hostApi'] == 0:
|
||||||
|
device_list.append(devInfo["name"])
|
||||||
|
return device_list
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/api/submit', methods=['post'])
|
||||||
|
def api_submit():
|
||||||
|
data = request.values.get('data')
|
||||||
|
# print(data)
|
||||||
|
config_data = json.loads(data)
|
||||||
|
config_util.save_config(config_data['config'])
|
||||||
|
return '{"result":"successful"}'
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/api/get-data', methods=['post'])
|
||||||
|
def api_get_data():
|
||||||
|
wsa_server.get_web_instance().add_cmd({
|
||||||
|
"voiceList": [
|
||||||
|
{"id": EnumVoice.XIAO_XIAO.name, "name": "晓晓"},
|
||||||
|
{"id": EnumVoice.YUN_XI.name, "name": "云溪"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
wsa_server.get_web_instance().add_cmd({"deviceList": __get_device_list()})
|
||||||
|
return json.dumps({'config': config_util.config})
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/api/start-live', methods=['post'])
|
||||||
|
def api_start_live():
|
||||||
|
# time.sleep(5)
|
||||||
|
fay_booter.start()
|
||||||
|
time.sleep(1)
|
||||||
|
wsa_server.get_web_instance().add_cmd({"liveState": 1})
|
||||||
|
return '{"result":"successful"}'
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/api/stop-live', methods=['post'])
|
||||||
|
def api_stop_live():
|
||||||
|
# time.sleep(1)
|
||||||
|
fay_booter.stop()
|
||||||
|
time.sleep(1)
|
||||||
|
wsa_server.get_web_instance().add_cmd({"liveState": 0})
|
||||||
|
return '{"result":"successful"}'
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/', methods=['get'])
|
||||||
|
def home_get():
|
||||||
|
return __get_template()
|
||||||
|
|
||||||
|
|
||||||
|
@__app.route('/', methods=['post'])
|
||||||
|
def home_post():
|
||||||
|
return __get_template()
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
MyThread(target=__app.run).start()
|
261
gui/static/css/index.css
Normal file
261
gui/static/css/index.css
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
#app {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* flex-wrap: wrap; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_box {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 915px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .left_top {
|
||||||
|
width: 915px;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_top_p {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_top {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_left {
|
||||||
|
width: 443px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_left ul {}
|
||||||
|
|
||||||
|
.character_left ul li {
|
||||||
|
display: flex;
|
||||||
|
height: 51.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_left ul li p {
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_left ul li .el-input {
|
||||||
|
width: 320px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_right {
|
||||||
|
display: flex;
|
||||||
|
width: 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_right ul {
|
||||||
|
width: 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_right ul li {
|
||||||
|
display: flex;
|
||||||
|
width: 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_right ul li p {
|
||||||
|
width: 120px;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_right ul li .el-slider__runway {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
.character_right ul li .el-select {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
.character_box {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_box p {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character_box .el-input {
|
||||||
|
width: 730px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title h2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 75px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box {
|
||||||
|
width: 915px;
|
||||||
|
/*height: 260px;*/
|
||||||
|
margin-top: 15px;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box p {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source {}
|
||||||
|
|
||||||
|
.left_box .source ul {}
|
||||||
|
|
||||||
|
.left_box .source ul li {}
|
||||||
|
|
||||||
|
.left_box .source ul .url {
|
||||||
|
width: 750px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .url .el-switch {
|
||||||
|
position: relative;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .url p {
|
||||||
|
width: 85px;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .url .el-input {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.left_box .source ul .url .el-select {
|
||||||
|
height: 40px;
|
||||||
|
width: 750px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .but {
|
||||||
|
width: 750px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .but .el-button {
|
||||||
|
margin: 20px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .p_red {
|
||||||
|
width: 750px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_box .source ul .p_red p {
|
||||||
|
color: red;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 915px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main {
|
||||||
|
width: 915px;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul {
|
||||||
|
width: 915px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul li {
|
||||||
|
width: 915px;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul li p {
|
||||||
|
width: 128px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul li .el-input {
|
||||||
|
width: 666px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul li .upload-demo {
|
||||||
|
width: 666px;
|
||||||
|
}
|
||||||
|
.right_main ul li .el-textarea {
|
||||||
|
width: 666px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_main ul li .el-switch {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
.el-input__inner {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #606266;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: inherit;
|
||||||
|
height: 43px;
|
||||||
|
line-height: 40px;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0 15px;
|
||||||
|
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.el-input.is-disabled .el-input__inner {
|
||||||
|
background-color: #F5F7FA;
|
||||||
|
border-color: #E4E7ED;
|
||||||
|
color: #000206 !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
447
gui/static/js/index.js
Normal file
447
gui/static/js/index.js
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
testlist: [
|
||||||
|
{
|
||||||
|
tab_name: "first",
|
||||||
|
name: "first",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab_name: "2",
|
||||||
|
name: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab_name: "3",
|
||||||
|
name: "3",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fileList: {},
|
||||||
|
panel_msg: "",
|
||||||
|
play_sound_enabled: false,
|
||||||
|
source_liveRoom_enabled: false,
|
||||||
|
source_liveRoom_url: '',
|
||||||
|
source_record_enabled: false,
|
||||||
|
source_record_device: '',
|
||||||
|
attribute_name: "",
|
||||||
|
attribute_gender: "",
|
||||||
|
attribute_age: "",
|
||||||
|
attribute_birth: "",
|
||||||
|
attribute_zodiac: "",
|
||||||
|
attribute_constellation: "",
|
||||||
|
attribute_job: "",
|
||||||
|
attribute_hobby: "",
|
||||||
|
attribute_contact: "",
|
||||||
|
attribute_voice: "",
|
||||||
|
interact_perception_gift: 0,
|
||||||
|
interact_perception_follow: 0,
|
||||||
|
interact_perception_join: 0,
|
||||||
|
interact_perception_chat: 0,
|
||||||
|
interact_perception_indifferent: 0,
|
||||||
|
interact_maxInteractTime: 15,
|
||||||
|
interact_QnA: "",
|
||||||
|
items_data: [],
|
||||||
|
live_state: 0,
|
||||||
|
device_list: [],
|
||||||
|
// device_list: [
|
||||||
|
// {
|
||||||
|
// value: '选项1',
|
||||||
|
// label: '麦克风'
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
voice_list: [],
|
||||||
|
options: [{
|
||||||
|
value: '选项1',
|
||||||
|
label: '黄金糕'
|
||||||
|
}, {
|
||||||
|
value: '选项2',
|
||||||
|
label: '双皮奶'
|
||||||
|
}],
|
||||||
|
activeName: 'first',
|
||||||
|
|
||||||
|
editableTabsValue: '1',
|
||||||
|
tabIndex: 1,
|
||||||
|
editableTabs: [{
|
||||||
|
title: 'Tab 1',
|
||||||
|
name: '1',
|
||||||
|
content: 'Tab 1 content'
|
||||||
|
}, {
|
||||||
|
title: 'Tab 2',
|
||||||
|
name: '2',
|
||||||
|
content: 'Tab 2 content'
|
||||||
|
}],
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabsEdit(targetName, action) {
|
||||||
|
if (action === 'add') {
|
||||||
|
let newTabName = ++this.tabIndex + '';
|
||||||
|
this.items_data.push({
|
||||||
|
tab_name: newTabName,
|
||||||
|
enabled: false,
|
||||||
|
name: "",
|
||||||
|
explain: {
|
||||||
|
intro: "",
|
||||||
|
usage: "",
|
||||||
|
price: "",
|
||||||
|
discount: "",
|
||||||
|
promise: "",
|
||||||
|
character: ""
|
||||||
|
},
|
||||||
|
demoVideo: "",
|
||||||
|
QnA: ""
|
||||||
|
});
|
||||||
|
this.editableTabsValue = newTabName;
|
||||||
|
}
|
||||||
|
if (action === 'remove') {
|
||||||
|
let tabs = this.items_data;
|
||||||
|
let activeName = this.editableTabsValue;
|
||||||
|
if (activeName === targetName) {
|
||||||
|
tabs.forEach((tab, index) => {
|
||||||
|
if (tab.tab_name === targetName) {
|
||||||
|
let nextTab = tabs[index + 1] || tabs[index - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.editableTabsValue = activeName;
|
||||||
|
this.items_data = tabs.filter(tab => tab.tab_name !== targetName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
alert("run...")
|
||||||
|
},
|
||||||
|
formatTooltip(val) {
|
||||||
|
return val / 100;
|
||||||
|
},
|
||||||
|
handleChange(value) {
|
||||||
|
console.log(value);
|
||||||
|
},
|
||||||
|
handleClick(tab, event) {
|
||||||
|
console.log(tab, event);
|
||||||
|
},
|
||||||
|
handleRemove(file, fileList) {
|
||||||
|
console.log(file, fileList);
|
||||||
|
},
|
||||||
|
handlePreview(file) {
|
||||||
|
console.log(file);
|
||||||
|
},
|
||||||
|
onExceed() {
|
||||||
|
},
|
||||||
|
beforeRemove() {
|
||||||
|
},
|
||||||
|
handleExceed() {
|
||||||
|
},
|
||||||
|
connectWS() {
|
||||||
|
let _this = this;
|
||||||
|
let socket = new WebSocket('ws://localhost:10003')
|
||||||
|
socket.onopen = function () {
|
||||||
|
// console.log('客户端连接上了服务器');
|
||||||
|
}
|
||||||
|
socket.onmessage = function (e) {
|
||||||
|
// console.log(" --> " + e.data)
|
||||||
|
let data = JSON.parse(e.data)
|
||||||
|
_this.live_broadcast = (data.time % 2) === 0
|
||||||
|
let liveState = data.liveState
|
||||||
|
if (liveState !== undefined) {
|
||||||
|
_this.live_state = liveState
|
||||||
|
if (liveState === 1) {
|
||||||
|
_this.sendSuccessMsg("已开启!")
|
||||||
|
} else if (liveState === 0) {
|
||||||
|
_this.sendSuccessMsg("已关闭!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let voiceList = data.voiceList
|
||||||
|
if (voiceList !== undefined) {
|
||||||
|
voice_list = []
|
||||||
|
for (let i = 0; i < voiceList.length; i++) {
|
||||||
|
voice_list[i] = {
|
||||||
|
value: voiceList[i].id,
|
||||||
|
label: voiceList[i].name
|
||||||
|
}
|
||||||
|
_this.voice_list = voice_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let deviceList = data.deviceList
|
||||||
|
if (deviceList !== undefined) {
|
||||||
|
device_list = []
|
||||||
|
for (let i = 0; i < deviceList.length; i++) {
|
||||||
|
device_list[i] = {
|
||||||
|
value: deviceList[i],
|
||||||
|
label: deviceList[i]
|
||||||
|
}
|
||||||
|
_this.device_list = device_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let panelMsg = data.panelMsg
|
||||||
|
if (panelMsg !== undefined) {
|
||||||
|
_this.panel_msg = panelMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getData() {
|
||||||
|
let _this = this;
|
||||||
|
let url = "http://127.0.0.1:5000/api/get-data";
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("post", url)
|
||||||
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
||||||
|
xhr.send()
|
||||||
|
let executed = false
|
||||||
|
xhr.onreadystatechange = async function () {
|
||||||
|
if (!executed && xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
if (xhr.responseText.length > 0) {
|
||||||
|
let data = await eval('(' + xhr.responseText + ')')
|
||||||
|
let config = data["config"]
|
||||||
|
let source = config["source"]
|
||||||
|
let attribute = config["attribute"]
|
||||||
|
let interact = config["interact"]
|
||||||
|
let perception = interact["perception"]
|
||||||
|
let items = config["items"]
|
||||||
|
_this.play_sound_enabled = interact["playSound"]
|
||||||
|
_this.source_liveRoom_enabled = source["liveRoom"]["enabled"]
|
||||||
|
_this.source_liveRoom_url = source["liveRoom"]["url"]
|
||||||
|
_this.source_record_enabled = source["record"]["enabled"]
|
||||||
|
_this.source_record_device = source["record"]["device"]
|
||||||
|
_this.attribute_name = attribute["name"]
|
||||||
|
_this.attribute_gender = attribute["gender"]
|
||||||
|
_this.attribute_age = attribute["age"]
|
||||||
|
_this.attribute_birth = attribute["birth"]
|
||||||
|
_this.attribute_zodiac = attribute["zodiac"]
|
||||||
|
_this.attribute_constellation = attribute["constellation"]
|
||||||
|
_this.attribute_job = attribute["job"]
|
||||||
|
_this.attribute_hobby = attribute["hobby"]
|
||||||
|
_this.attribute_contact = attribute["contact"]
|
||||||
|
_this.attribute_voice = attribute["voice"]
|
||||||
|
_this.interact_perception_gift = parseInt(perception["gift"])
|
||||||
|
_this.interact_perception_follow = perception["follow"]
|
||||||
|
_this.interact_perception_join = perception["join"]
|
||||||
|
_this.interact_perception_chat = perception["chat"]
|
||||||
|
_this.interact_perception_indifferent = perception["indifferent"]
|
||||||
|
_this.interact_maxInteractTime = interact["maxInteractTime"]
|
||||||
|
_this.interact_QnA = interact["QnA"]
|
||||||
|
let item_data_list = []
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
let item = items[i]
|
||||||
|
let _tab_name = "first"
|
||||||
|
if (i > 0) {
|
||||||
|
_tab_name = i.toString()
|
||||||
|
}
|
||||||
|
item_data_list[i] = {
|
||||||
|
tab_name: _tab_name,
|
||||||
|
enabled: item.enabled,
|
||||||
|
name: item.name,
|
||||||
|
explain: {
|
||||||
|
intro: item.explain.intro,
|
||||||
|
usage: item.explain.usage,
|
||||||
|
price: item.explain.price,
|
||||||
|
discount: item.explain.discount,
|
||||||
|
promise: item.explain.promise,
|
||||||
|
character: item.explain.character
|
||||||
|
},
|
||||||
|
demoVideo: item.demoVideo,
|
||||||
|
QnA: item.QnA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_this.items_data = item_data_list
|
||||||
|
console.log(_this.items_data);
|
||||||
|
executed = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postData() {
|
||||||
|
let url = "http://127.0.0.1:5000/api/submit";
|
||||||
|
let send_data = {
|
||||||
|
"config": {
|
||||||
|
"source": {
|
||||||
|
"liveRoom": {
|
||||||
|
"enabled": this.source_liveRoom_enabled,
|
||||||
|
"url": this.source_liveRoom_url
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"enabled": this.source_record_enabled,
|
||||||
|
"device": this.source_record_device
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"voice": this.attribute_voice,
|
||||||
|
"name": this.attribute_name,
|
||||||
|
"gender": this.attribute_gender,
|
||||||
|
"age": this.attribute_age,
|
||||||
|
"birth": this.attribute_birth,
|
||||||
|
"zodiac": this.attribute_zodiac,
|
||||||
|
"constellation": this.attribute_constellation,
|
||||||
|
"job": this.attribute_job,
|
||||||
|
"hobby": this.attribute_hobby,
|
||||||
|
"contact": this.attribute_contact
|
||||||
|
},
|
||||||
|
"interact": {
|
||||||
|
"playSound": this.play_sound_enabled,
|
||||||
|
"QnA": this.interact_QnA,
|
||||||
|
"maxInteractTime": this.interact_maxInteractTime,
|
||||||
|
"perception": {
|
||||||
|
"gift": this.interact_perception_gift,
|
||||||
|
"follow": this.interact_perception_follow,
|
||||||
|
"join": this.interact_perception_join,
|
||||||
|
"chat": this.interact_perception_chat,
|
||||||
|
"indifferent": this.interact_perception_indifferent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": [],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (let i = 0; i < this.items_data.length; i++) {
|
||||||
|
let item = this.items_data[i]
|
||||||
|
send_data.config.items[i] = {
|
||||||
|
enabled: item.enabled,
|
||||||
|
name: item.name,
|
||||||
|
explain: {
|
||||||
|
intro: item.explain.intro,
|
||||||
|
usage: item.explain.usage,
|
||||||
|
price: item.explain.price,
|
||||||
|
discount: item.explain.discount,
|
||||||
|
promise: item.explain.promise,
|
||||||
|
character: item.explain.character
|
||||||
|
},
|
||||||
|
demoVideo: item.demoVideo,
|
||||||
|
QnA: item.QnA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("post", url)
|
||||||
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
||||||
|
xhr.send('data=' + JSON.stringify(send_data))
|
||||||
|
let executed = false
|
||||||
|
xhr.onreadystatechange = async function () {
|
||||||
|
if (!executed && xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
let data = await eval('(' + xhr.responseText + ')')
|
||||||
|
console.log("data: " + data['result'])
|
||||||
|
executed = true
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sendSuccessMsg("配置已保存!")
|
||||||
|
},
|
||||||
|
postStartLive() {
|
||||||
|
this.postData()
|
||||||
|
this.live_state = 2
|
||||||
|
let url = "http://127.0.0.1:5000/api/start-live";
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("post", url)
|
||||||
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
||||||
|
xhr.send()
|
||||||
|
},
|
||||||
|
postStopLive() {
|
||||||
|
this.live_state = 3
|
||||||
|
let url = "http://127.0.0.1:5000/api/stop-live";
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("post", url)
|
||||||
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
||||||
|
xhr.send()
|
||||||
|
},
|
||||||
|
isEmptyItem(data) {
|
||||||
|
let isEmpty = true
|
||||||
|
let explain = data["explain"]
|
||||||
|
for (let key in data) {
|
||||||
|
let value = data[key]
|
||||||
|
if (key !== "tab_name" && value.constructor === String && value.length > 0) {
|
||||||
|
isEmpty = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in explain) {
|
||||||
|
let value = explain[key]
|
||||||
|
if (value.constructor === String && value.length > 0) {
|
||||||
|
isEmpty = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isEmpty
|
||||||
|
},
|
||||||
|
lastItemIsEmpty() {
|
||||||
|
return this.isEmptyItem(this.items_data[this.items_data.length - 1])
|
||||||
|
},
|
||||||
|
uuid() {
|
||||||
|
let s = []
|
||||||
|
let hexDigits = '0123456789abcdef'
|
||||||
|
for (let i = 0; i < 36; i++) {
|
||||||
|
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
|
||||||
|
}
|
||||||
|
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
|
||||||
|
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
|
||||||
|
s[8] = s[13] = s[18] = s[23] = '-'
|
||||||
|
|
||||||
|
let uuid = s.join('')
|
||||||
|
return uuid
|
||||||
|
},
|
||||||
|
runnnable() {
|
||||||
|
setTimeout(() => {
|
||||||
|
let _this = this
|
||||||
|
let item_data_list = []
|
||||||
|
let changed = false
|
||||||
|
let index = 0
|
||||||
|
for (let i = 0; i < _this.items_data.length; i++) {
|
||||||
|
let data = _this.items_data[i]
|
||||||
|
if (i === (_this.items_data.length - 1) || !this.isEmptyItem(data)) {
|
||||||
|
item_data_list[index] = _this.items_data[i]
|
||||||
|
index++
|
||||||
|
} else {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.lastItemIsEmpty()) {
|
||||||
|
changed = true
|
||||||
|
item_data_list.push({
|
||||||
|
tab_name: this.uuid(),
|
||||||
|
enabled: false,
|
||||||
|
name: "",
|
||||||
|
explain: {
|
||||||
|
intro: "",
|
||||||
|
usage: "",
|
||||||
|
price: "",
|
||||||
|
discount: "",
|
||||||
|
promise: "",
|
||||||
|
character: ""
|
||||||
|
},
|
||||||
|
demoVideo: "",
|
||||||
|
QnA: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
_this.items_data = item_data_list
|
||||||
|
console.log("修改了!" + _this.items_data.length)
|
||||||
|
}
|
||||||
|
this.runnnable()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
|
sendSuccessMsg(text) {
|
||||||
|
this.$notify({
|
||||||
|
title: '成功',
|
||||||
|
message: text,
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let _this = this;
|
||||||
|
_this.getData();
|
||||||
|
_this.connectWS()
|
||||||
|
// _this.runnnable()
|
||||||
|
// _this.items_data.push({});
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
items_data() {
|
||||||
|
// console.log("items_data 改变了");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
25
gui/static/js/self-adaption.js
Normal file
25
gui/static/js/self-adaption.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
window.onload = function () {
|
||||||
|
document.body.style.zoom = "normal";//避免zoom尺寸叠加
|
||||||
|
let scale = document.body.clientWidth / 1920;
|
||||||
|
document.body.style.zoom = scale;
|
||||||
|
}; (function () {
|
||||||
|
var throttle = function (type, name, obj) {
|
||||||
|
obj = obj || window;
|
||||||
|
var running = false;
|
||||||
|
var func = function () {
|
||||||
|
if (running) { return; }
|
||||||
|
running = true;
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
obj.dispatchEvent(new CustomEvent(name));
|
||||||
|
running = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
obj.addEventListener(type, func);
|
||||||
|
};
|
||||||
|
throttle("resize", "optimizedResize");
|
||||||
|
})();
|
||||||
|
window.addEventListener("optimizedResize", function () {
|
||||||
|
document.body.style.zoom = "normal";
|
||||||
|
let scale = document.body.clientWidth / 1920;
|
||||||
|
document.body.style.zoom = scale;
|
||||||
|
});
|
242
gui/templates/index.html
Normal file
242
gui/templates/index.html
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- index.css -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static',filename='css/index.css') }}"></link>
|
||||||
|
<!-- 引入element-ui样式 -->
|
||||||
|
<!-- <link rel="stylesheet" href="{{ url_for('static',filename='css/element.css') }}"></link> -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||||
|
|
||||||
|
<title>自动商品介绍控制器</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="title">
|
||||||
|
<h2>数字人控制器</h2>
|
||||||
|
</div>
|
||||||
|
<div class="main_box">
|
||||||
|
<div class="left">
|
||||||
|
<div class="left_top">
|
||||||
|
<p class="left_top_p">人设:</p>
|
||||||
|
<div class="character">
|
||||||
|
<div class="character_top">
|
||||||
|
<div class="character_left">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>姓名:</p>
|
||||||
|
<el-input v-model="attribute_name" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>性别:</p>
|
||||||
|
<el-input v-model="attribute_gender" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>年龄:</p>
|
||||||
|
<el-input v-model="attribute_age" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<p>出生地:</p>
|
||||||
|
<el-input v-model="attribute_birth" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>生肖:</p>
|
||||||
|
<el-input v-model="attribute_zodiac" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>星座:</p>
|
||||||
|
<el-input v-model="attribute_constellation" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>职业:</p>
|
||||||
|
<el-input v-model="attribute_job" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>喜好:</p>
|
||||||
|
<el-input v-model="attribute_hobby" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<p>联系方式:</p>
|
||||||
|
<el-input v-model="attribute_contact" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="character_right">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>送礼敏感度:</p>
|
||||||
|
|
||||||
|
<el-slider v-model="interact_perception_gift"></el-slider>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>关注敏感度:</p>
|
||||||
|
<el-slider v-model="interact_perception_follow"></el-slider>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>进入敏感度:</p>
|
||||||
|
<el-slider v-model="interact_perception_join"></el-slider>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>留言敏感度:</p>
|
||||||
|
<el-slider v-model="interact_perception_chat"></el-slider>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>冷场敏感度:</p>
|
||||||
|
<el-slider v-model="interact_perception_indifferent"></el-slider>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>单次互动时长:</p>
|
||||||
|
<el-input-number v-model="interact_maxInteractTime" @change="handleChange" :min="1" :max="60" label="描述文字"></el-input-number>
|
||||||
|
<span style=" padding-left: 10px;
|
||||||
|
line-height: 40px;">秒</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>声音选择:{{attribute_voice}}</p>
|
||||||
|
<el-select v-model="attribute_voice" placeholder="请选择">
|
||||||
|
<el-option v-for="item in voice_list" :key="item.value" :label="item.label" :value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</li>
|
||||||
|
<br>
|
||||||
|
<li>
|
||||||
|
<p>使用面板播放:</p>
|
||||||
|
<el-switch v-model="play_sound_enabled" active-color="#13ce66" inactive-color="#ff4949">
|
||||||
|
</el-switch>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="character_box">
|
||||||
|
<p>Q&A文件:</p>
|
||||||
|
<el-input v-model="interact_QnA" placeholder="请输入内容"></el-input>
|
||||||
|
<!-- <el-upload class="upload-demo" action="http://127.0.0.1:5000/"-->
|
||||||
|
<!-- :on-success="handlePreview">-->
|
||||||
|
<!-- <el-input v-model="interact_QnA" placeholder="请输入内容"></el-input>-->
|
||||||
|
<!-- </el-upload>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="left_box">
|
||||||
|
<p>接收来源:</p>
|
||||||
|
<div class="source">
|
||||||
|
<ul>
|
||||||
|
<li class="url">
|
||||||
|
<el-switch v-model="source_liveRoom_enabled" active-color="#13ce66" inactive-color="#ff4949">
|
||||||
|
</el-switch>
|
||||||
|
<p>抖 音</p>
|
||||||
|
<el-input v-model="source_liveRoom_url" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li class="url">
|
||||||
|
<el-switch v-model="source_record_enabled" active-color="#13ce66" inactive-color="#ff4949">
|
||||||
|
</el-switch>
|
||||||
|
<p>麦克风</p>
|
||||||
|
<el-select v-model="source_record_device" placeholder="请选择">
|
||||||
|
<el-option v-for="item in device_list" :key="item.value" :label="item.label"
|
||||||
|
:value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</li>
|
||||||
|
<li class="url">
|
||||||
|
|
||||||
|
<p style="margin-left: 40px">消 息</p>
|
||||||
|
<el-input v-model="panel_msg" :disabled="true"></el-input>
|
||||||
|
</li>
|
||||||
|
<li class="but">
|
||||||
|
<el-button v-if="live_state == 1" type="success" style="width:200px" @click=postStopLive()>关闭(运行中)</el-button>
|
||||||
|
<el-button v-else-if="live_state == 2" type="primary" plain disabled style="width:200px">正在开启...</el-button>
|
||||||
|
<el-button v-else-if="live_state == 3" type="success" plain disabled style="width:200px">正在关闭...</el-button>
|
||||||
|
<el-button v-else type="primary" style="width:200px" @click=postStartLive()>开启</el-button>
|
||||||
|
<el-button type="button" style="width:200px" @click=postData()>保存配置</el-button>
|
||||||
|
</li>
|
||||||
|
<li class="p_red">
|
||||||
|
<p>注:启动后请选中场景客户端,让其前端运行,否则可能会卡顿,或者无声音。</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="right_main">
|
||||||
|
<el-tabs v-model="activeName" type="card" @tab-click="handleClick" editable @edit="handleTabsEdit">
|
||||||
|
<el-tab-pane :label="'商品'+(index+1)" :name="itme.tab_name" v-for="(itme, index) in items_data" :key="index">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>名称:</p>
|
||||||
|
<el-input v-model='itme.name' placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>商品简介:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.intro'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>使用场景:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.usage'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>售价说明:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.price'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>促销:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.discount'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>主播担保:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.promise'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>商品特点:</p>
|
||||||
|
<el-input type="textarea" :rows="3" placeholder="请输入内容" v-model='itme.explain.character'>
|
||||||
|
</el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>展示视频:</p>
|
||||||
|
<el-input v-model="itme.demoVideo" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Q&A文件:</p>
|
||||||
|
<el-input v-model="itme.QnA" placeholder="请输入内容"></el-input>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>是否启用:</p>
|
||||||
|
<el-switch v-model='itme.enabled' active-color="#13ce66" inactive-color="#ff4949">
|
||||||
|
</el-switch>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<br v-if="items_data.length==0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<!-- 开发环境vue.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
<!-- 发行环境vue.js -->
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> -->
|
||||||
|
<!-- 引入element-ui组件库 -->
|
||||||
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||||
|
<!-- <script src="{{ url_for('static',filename='js/element.js') }}"></script> -->
|
||||||
|
<!-- index.js -->
|
||||||
|
|
||||||
|
<script src="{{ url_for('static',filename='js/index.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static',filename='js/self-adaption.js') }}"></script>
|
||||||
|
|
||||||
|
</html>
|
81
gui/window.py
Normal file
81
gui/window.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout
|
||||||
|
from PyQt5.QtWidgets import QGroupBox
|
||||||
|
from PyQt5.QtWebEngineWidgets import *
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
SigSendMessageToJS = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(MainWindow, self).__init__()
|
||||||
|
# self.setWindowFlags(Qt.WindowType.WindowShadeButtonHint)
|
||||||
|
self.setWindowTitle('Fay')
|
||||||
|
# self.setFixedSize(16 * 80, 9 * 80)
|
||||||
|
self.setGeometry(0, 0, 16 * 70, 9 * 70)
|
||||||
|
self.showMaximized()
|
||||||
|
# self.center()
|
||||||
|
self.browser = QWebEngineView()
|
||||||
|
self.browser.load(QUrl('http://127.0.0.1:5000'))
|
||||||
|
self.setCentralWidget(self.browser)
|
||||||
|
MyThread(target=self.runnable).start()
|
||||||
|
|
||||||
|
def runnable(self):
|
||||||
|
while True:
|
||||||
|
if not self.isVisible():
|
||||||
|
# try:
|
||||||
|
# wsa_server.get_instance().stop_server()
|
||||||
|
# wsa_server.get_web_instance().stop_server()
|
||||||
|
# thread_manager.stopAll()
|
||||||
|
# except BaseException as e:
|
||||||
|
# print(e)
|
||||||
|
os.system("taskkill /F /PID {}".format(os.getpid()))
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
def center(self):
|
||||||
|
screen = QtWidgets.QDesktopWidget().screenGeometry()
|
||||||
|
size = self.geometry()
|
||||||
|
self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
pass
|
||||||
|
# if event.key() == Qt.Key_F12:
|
||||||
|
# self.s = TDevWindow()
|
||||||
|
# self.s.show()
|
||||||
|
# self.browser.page().setDevToolsPage(self.s.mpJSWebView.page())
|
||||||
|
|
||||||
|
def OnReceiveMessageFromJS(self, strParameter):
|
||||||
|
if not strParameter:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class TDevWindow(QDialog):
|
||||||
|
def __init__(self):
|
||||||
|
super(TDevWindow, self).__init__()
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.mpJSWebView = QWebEngineView(self)
|
||||||
|
self.url = 'https://www.baidu.com/'
|
||||||
|
self.mpJSWebView.page().load(QUrl(self.url))
|
||||||
|
self.mpJSWebView.show()
|
||||||
|
|
||||||
|
self.pJSTotalVLayout = QVBoxLayout()
|
||||||
|
self.pJSTotalVLayout.setSpacing(0)
|
||||||
|
self.pJSTotalVLayout.addWidget(self.mpJSWebView)
|
||||||
|
self.pWebGroup = QGroupBox('Web View', self)
|
||||||
|
self.pWebGroup.setLayout(self.pJSTotalVLayout)
|
||||||
|
|
||||||
|
self.mainLayout = QHBoxLayout()
|
||||||
|
self.mainLayout.setSpacing(5)
|
||||||
|
self.mainLayout.addWidget(self.pWebGroup)
|
||||||
|
self.setLayout(self.mainLayout)
|
||||||
|
self.setMinimumSize(800, 800)
|
BIN
images/UE.png
Normal file
BIN
images/UE.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
BIN
images/controller.png
Normal file
BIN
images/controller.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
39
main.py
Normal file
39
main.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PyQt5 import QtGui
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from ai_module import ali_nls
|
||||||
|
from core import wsa_server
|
||||||
|
from gui import flask_server
|
||||||
|
from gui.window import MainWindow
|
||||||
|
from utils import config_util
|
||||||
|
|
||||||
|
|
||||||
|
def __clear_samples():
|
||||||
|
if not os.path.exists("./samples"):
|
||||||
|
os.mkdir("./samples")
|
||||||
|
for file_name in os.listdir('./samples'):
|
||||||
|
if file_name.startswith('sample-') and file_name.endswith('.mp3'):
|
||||||
|
os.remove('./samples/' + file_name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
__clear_samples()
|
||||||
|
config_util.load_config()
|
||||||
|
# fay_booter.start()
|
||||||
|
ws_server = wsa_server.new_instance(port=10002)
|
||||||
|
ws_server.start_server()
|
||||||
|
web_ws_server = wsa_server.new_web_instance(port=10003)
|
||||||
|
web_ws_server.start_server()
|
||||||
|
|
||||||
|
ali_nls.start()
|
||||||
|
|
||||||
|
flask_server.start()
|
||||||
|
# MyThread(target=runnable).start()
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setWindowIcon(QtGui.QIcon('icon.png'))
|
||||||
|
win = MainWindow()
|
||||||
|
win.show()
|
||||||
|
app.exit(app.exec_())
|
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
requests~=2.26.0
|
||||||
|
selenium~=4.1.3
|
||||||
|
numpy~=1.19.5
|
||||||
|
pyaudio~=0.2.11
|
||||||
|
websockets~=10.2
|
||||||
|
ws4py~=0.5.1
|
||||||
|
pyqt5~=5.15.6
|
||||||
|
flask~=2.1.1
|
||||||
|
openpyxl~=3.0.9
|
||||||
|
pygame~=2.1.2
|
||||||
|
flask_cors~=3.0.10
|
||||||
|
PyQtWebEngine~=5.15.5
|
||||||
|
eyed3~=0.9.6
|
||||||
|
websocket~=0.2.1
|
||||||
|
websocket-client~=1.3.2
|
||||||
|
azure-cognitiveservices-speech~=1.21.0
|
||||||
|
aliyun-python-sdk-core==2.13.3
|
43
scheduler/thread_manager.py
Normal file
43
scheduler/thread_manager.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import ctypes
|
||||||
|
import threading
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class MyThread(Thread):
|
||||||
|
def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None):
|
||||||
|
Thread.__init__(self, group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon)
|
||||||
|
add_thread(self)
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
# returns id of the respective thread
|
||||||
|
if hasattr(self, '_thread_id'):
|
||||||
|
return self._thread_id
|
||||||
|
for id, thread in threading._active.items():
|
||||||
|
if thread is self:
|
||||||
|
return id
|
||||||
|
|
||||||
|
def raise_exception(self):
|
||||||
|
thread_id = self.get_id()
|
||||||
|
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
|
||||||
|
if res > 1:
|
||||||
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
|
||||||
|
print('Exception raise failure')
|
||||||
|
|
||||||
|
|
||||||
|
__thread_list = []
|
||||||
|
|
||||||
|
|
||||||
|
def add_thread(thread: MyThread):
|
||||||
|
if thread not in __thread_list:
|
||||||
|
__thread_list.append(thread)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_thread(thread: MyThread):
|
||||||
|
if thread in __thread_list:
|
||||||
|
__thread_list.remove(thread)
|
||||||
|
|
||||||
|
|
||||||
|
def stopAll():
|
||||||
|
for thread in __thread_list:
|
||||||
|
thread.raise_exception()
|
||||||
|
thread.join()
|
20
system.conf
Normal file
20
system.conf
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[system]
|
||||||
|
# ChromeDriver 路径
|
||||||
|
chrome_driver=./bin/chromedriver.exe
|
||||||
|
|
||||||
|
[key]
|
||||||
|
# 阿里云 实时语音识别 服务密钥
|
||||||
|
ali_nls_key_id=
|
||||||
|
ali_nls_key_secret=
|
||||||
|
ali_nls_app_key=
|
||||||
|
|
||||||
|
# 微软 文字转语音 服务密钥
|
||||||
|
ms_tts_key=
|
||||||
|
|
||||||
|
# 讯飞 自然语言处理 服务密钥
|
||||||
|
xf_aiui_app_id=
|
||||||
|
xf_aiui_api_key=
|
||||||
|
|
||||||
|
# 讯飞 情绪分析 服务密钥
|
||||||
|
xf_ltp_app_id=
|
||||||
|
xf_ltp_api_key=
|
53
utils/config_util.py
Normal file
53
utils/config_util.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import codecs
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
config: json = None
|
||||||
|
system_config: ConfigParser = None
|
||||||
|
system_chrome_driver = None
|
||||||
|
key_ali_nls_key_id = None
|
||||||
|
key_ali_nls_key_secret = None
|
||||||
|
key_ali_nls_app_key = None
|
||||||
|
key_ms_tts_key = None
|
||||||
|
key_xf_aiui_app_id = None
|
||||||
|
key_xf_aiui_api_key = None
|
||||||
|
key_xf_ltp_app_id = None
|
||||||
|
key_xf_ltp_api_key = None
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
global config
|
||||||
|
global system_config
|
||||||
|
global system_chrome_driver
|
||||||
|
global key_ali_nls_key_id
|
||||||
|
global key_ali_nls_key_secret
|
||||||
|
global key_ali_nls_app_key
|
||||||
|
global key_ms_tts_key
|
||||||
|
global key_xf_aiui_app_id
|
||||||
|
global key_xf_aiui_api_key
|
||||||
|
global key_xf_ltp_app_id
|
||||||
|
global key_xf_ltp_api_key
|
||||||
|
|
||||||
|
system_config = ConfigParser()
|
||||||
|
system_config.read('system.conf', encoding='UTF-8')
|
||||||
|
system_chrome_driver = os.path.abspath(system_config.get('system', 'chrome_driver'))
|
||||||
|
key_ali_nls_key_id = system_config.get('key', 'ali_nls_key_id')
|
||||||
|
key_ali_nls_key_secret = system_config.get('key', 'ali_nls_key_secret')
|
||||||
|
key_ali_nls_app_key = system_config.get('key', 'ali_nls_app_key')
|
||||||
|
key_ms_tts_key = system_config.get('key', 'ms_tts_key')
|
||||||
|
key_xf_aiui_app_id = system_config.get('key', 'xf_aiui_app_id')
|
||||||
|
key_xf_aiui_api_key = system_config.get('key', 'xf_aiui_api_key')
|
||||||
|
key_xf_ltp_app_id = system_config.get('key', 'xf_ltp_app_id')
|
||||||
|
key_xf_ltp_api_key = system_config.get('key', 'xf_ltp_api_key')
|
||||||
|
|
||||||
|
config = json.load(codecs.open('config.json', encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config_data):
|
||||||
|
global config
|
||||||
|
config = config_data
|
||||||
|
file = codecs.open('config.json', mode='w', encoding='utf-8')
|
||||||
|
file.write(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')))
|
||||||
|
file.close()
|
||||||
|
# for line in json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')).split("\n"):
|
||||||
|
# print(line)
|
29
utils/storer.py
Normal file
29
utils/storer.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
from threading import Thread
|
||||||
|
import time
|
||||||
|
|
||||||
|
FILE_URL = "datas/data-" + time.strftime("%Y%m%d%H%M%S") + ".csv"
|
||||||
|
|
||||||
|
|
||||||
|
def __write_to_file(text):
|
||||||
|
if not os.path.exists("datas"):
|
||||||
|
os.mkdir("datas")
|
||||||
|
file = codecs.open(FILE_URL, 'a', 'utf-8')
|
||||||
|
file.write(text + "\n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def storage_live_interact(interact):
|
||||||
|
interact_type = interact[0]
|
||||||
|
user = interact[1].replace(',', ',')
|
||||||
|
msg = interact[2].replace(',', ',')
|
||||||
|
msg_type = {
|
||||||
|
0: '主播',
|
||||||
|
1: '发言',
|
||||||
|
2: '进入',
|
||||||
|
3: '送礼',
|
||||||
|
4: '关注'
|
||||||
|
}
|
||||||
|
timestamp = int(time.time() * 1000)
|
||||||
|
Thread(target=__write_to_file, args=["%s,%s,%s,%s\n" % (timestamp, msg_type[interact_type], user, msg)]).start()
|
39
utils/util.py
Normal file
39
utils/util.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from core import wsa_server
|
||||||
|
from scheduler.thread_manager import MyThread
|
||||||
|
|
||||||
|
LOGS_FILE_URL = "logs/log-" + time.strftime("%Y%m%d%H%M%S") + ".log"
|
||||||
|
|
||||||
|
|
||||||
|
def random_hex(length):
|
||||||
|
result = hex(random.randint(0, 16 ** length)).replace('0x', '').lower()
|
||||||
|
if len(result) < length:
|
||||||
|
result = '0' * (length - len(result)) + result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def __write_to_file(text):
|
||||||
|
if not os.path.exists("logs"):
|
||||||
|
os.mkdir("logs")
|
||||||
|
file = codecs.open(LOGS_FILE_URL, 'a', 'utf-8')
|
||||||
|
file.write(text + "\n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def printInfo(level, sender, text, send_time=-1):
|
||||||
|
if send_time < 0:
|
||||||
|
send_time = time.time()
|
||||||
|
format_time = time.strftime('%H:%M:%S', time.localtime(send_time))
|
||||||
|
logStr = '[{}][{}] {}'.format(format_time, sender, text)
|
||||||
|
print(logStr)
|
||||||
|
if level >= 3:
|
||||||
|
wsa_server.get_web_instance().add_cmd({"panelMsg": text})
|
||||||
|
MyThread(target=__write_to_file, args=[logStr]).start()
|
||||||
|
|
||||||
|
|
||||||
|
def log(level, text):
|
||||||
|
printInfo(level, "系统", text)
|
Loading…
Reference in New Issue
Block a user