mirror of https://gitlab.com/gpvkt/twitchtts.git
Merge branch 'main' of gitlab.com:gpvkt/twitchtts
This commit is contained in:
commit
7a0a1d4c03
|
@ -1,3 +1,4 @@
|
|||
config.yml
|
||||
build
|
||||
tts.spec
|
||||
tts.exe
|
||||
|
|
53
README.md
53
README.md
|
@ -1,6 +1,6 @@
|
|||
# Twitch TextToSpeech Bot
|
||||
|
||||
A simple Twitch TTS bot
|
||||
A simple Twitch TTS bot (Web Speech API)
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -48,7 +48,7 @@ whitelist:
|
|||
|
||||
**Explanation**:
|
||||
|
||||
### irc
|
||||
#### irc
|
||||
|
||||
* `channel`: Channel you want to monitor (e.g. #gpkvt)
|
||||
* `username`: The bots username (e.g. gpkvt)
|
||||
|
@ -56,18 +56,18 @@ whitelist:
|
|||
* `server`: Twitch IRC server to be used (default should be fine)
|
||||
* `clearmsg_timeout`: Time to wait for an moderator to delete a message, before it's added to the TTS queue
|
||||
|
||||
### http
|
||||
#### http
|
||||
|
||||
* `port`: Internal Webserver Port to listen to (e.g. 8080)
|
||||
* `bind`: Interface/IP to bind server to (e.g. localhost)
|
||||
|
||||
### bot
|
||||
#### bot
|
||||
|
||||
* `subonly`: If `True` only Subs can use TTS
|
||||
* `modonly`: If `True` only Mods can use TTS
|
||||
* `message_length`: Maximum allowed message length for TTS
|
||||
|
||||
### messages
|
||||
#### messages
|
||||
|
||||
* `too_long`: The bots reply if message exceeds `message_length`
|
||||
* `disabled`: The bots reply if TTS is disabled
|
||||
|
@ -77,11 +77,11 @@ whitelist:
|
|||
* `ready`: The bots init message
|
||||
* `says`: Prefix to add between username and message
|
||||
|
||||
### log
|
||||
#### log
|
||||
|
||||
* `level`: The loglevel, valid values are: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
||||
|
||||
### usermapping
|
||||
#### usermapping
|
||||
|
||||
Use this section to define key:value pairs of usernames. The first value is the Twitch username, the second value is how the bot should pronouce the user, when reading the message. This is helpfull if you have regulars with numbers or strangs chars in the name. You can add new/change entries on the fly without restarting the bot (changes took up to 60 seconds).
|
||||
|
||||
|
@ -100,7 +100,7 @@ whitelist:
|
|||
- bar
|
||||
```
|
||||
|
||||
To disable the whitelist remove it from `config.yml` completely. If you just leave `whitelist:` without entries, everyone must be whitelisted using `!ptts`. The permit is temporarily until the bot restarts or the user is removed from the (temporary) whitelist using `!dtts`.
|
||||
To disable the whitelist, remove it from `config.yml` completely. If you just leave `whitelist:` without entries, everyone must be whitelisted using `!ptts` (even broadcaster and mods). The permit is temporary until the bot restarts or (whichever happens first) if the user is removed from the whitelist using `!dtts`.
|
||||
|
||||
Please note: Usernames MUST be lowercase.
|
||||
|
||||
|
@ -116,3 +116,40 @@ Additional commands (broadcaster and mods only) are:
|
|||
* `!ton`: Turn TTS back on
|
||||
* `!dtts <username>`: Disable TTS for the given user
|
||||
* `!ptts <username>`: Allow TTS for the given user
|
||||
|
||||
# Voices
|
||||
|
||||
The voices available depends on your Operating System and/or browser. On some systems only a default voice is available and the `Select voice` dropdown might stay empty or will only show entries after you clicked the `Init` button. Some Android devices will show a huge list of voices, but sounds the same no matter which one you choose.
|
||||
|
||||
On Windows you can install additional voices via `Settings` > `Time & language` > `Speech` > `Add voices` or by simply run `Add speech voices`
|
||||
|
||||
## Help
|
||||
|
||||
Feel free to use the [Issuetracker](https://gitlab.com/gpvkt/twitchtts/-/issues) if you experience any problems.
|
||||
|
||||
## Authors
|
||||
|
||||
[@gpkvt](https://gitlab.com/gpvkt)
|
||||
|
||||
## Version History
|
||||
|
||||
There is no stable release, yet.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPLv3 License - see the LICENSE.md file for details
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
* [Python Software Foundation and contributors](https://www.python.org/)
|
||||
* [PyYAML](https://pyyaml.org/)
|
||||
* [pyinstaller](https://pyinstaller.org/)
|
||||
* [OpenJS Foundation and jQuery contributors](https://jquery.org/)
|
||||
* [Twitter Inc. and Bootstrap contributors](https://getbootstrap.com/)
|
||||
* [GERBrowny and community](https://twitch.tv/gerbrowny/)
|
||||
* [DerZugger and community](https://www.twitch.tv/derzugger/)
|
||||
* [Timmeh74 and community](https://www.twitch.tv/timmeh74/)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Twitch Interactive, Inc.
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 318 B |
22
tts.html
22
tts.html
|
@ -4,17 +4,16 @@
|
|||
<title>TTS</title>
|
||||
<script src="./jquery.js"></script>
|
||||
</head>
|
||||
<body class="container mt-5 bg-dark">
|
||||
<h1 class="text-light">Text to Speech</h1>
|
||||
<p class="lead text-light mt-4">Select Voice</p>
|
||||
<select id="voices" class="form-select bg-secondary text-light"></select>
|
||||
<div class="d-flex mt-4 text-light">
|
||||
<body class="container mt-2">
|
||||
<p class="lead mt-2">Select Voice</p>
|
||||
<select id="voices" class="form-select bg-transparent"></select>
|
||||
<div class="d-flex mt-2">
|
||||
<div>
|
||||
<p class="lead">Volume</p>
|
||||
<input type="range" min="0" max="1" value="1" step="0.1" id="volume" />
|
||||
<span id="volume-label" class="ms-2">1</span>
|
||||
</div>
|
||||
<div class="mx-5">
|
||||
<div class="mx-4">
|
||||
<p class="lead">Rate</p>
|
||||
<input type="range" min="0.1" max="10" value="1" id="rate" step="0.1" />
|
||||
<span id="rate-label" class="ms-2">1</span>
|
||||
|
@ -25,12 +24,11 @@
|
|||
<span id="pitch-label" class="ms-2">1</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--textarea id="tts" class="form-control bg-dark text-light mt-5" cols="30" rows="10" placeholder="Type here..."></textarea//-->
|
||||
<div class="mb-5">
|
||||
<button id="start" class="btn btn-success mt-5 me-3">Init</button>
|
||||
<button id="pause" class="btn btn-warning mt-5 me-3">Pause</button>
|
||||
<button id="resume" class="btn btn-info mt-5 me-3">Resume</button>
|
||||
<button id="cancel" class="btn btn-danger mt-5 me-3">Cancel</button>
|
||||
<div>
|
||||
<button id="start" class="btn btn-success mt-4 me-2">Init</button>
|
||||
<button id="pause" class="btn btn-warning mt-4 me-2">Pause</button>
|
||||
<button id="resume" class="btn btn-info mt-4 me-2">Resume</button>
|
||||
<button id="cancel" class="btn btn-danger mt-4 me-2">Cancel</button>
|
||||
</div>
|
||||
</body>
|
||||
<script src="./tts.js"></script>
|
||||
|
|
98
tts.py
98
tts.py
|
@ -1,6 +1,24 @@
|
|||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
TwitchTTS
|
||||
Copyright (C) 2022 gpkvt
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
import yaml
|
||||
import logging
|
||||
|
@ -20,18 +38,18 @@ class IRC:
|
|||
def __init__(self):
|
||||
self.irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.tts_denied = []
|
||||
if 'WHITELIST_USER' in conf:
|
||||
self.tts_allowed = conf['WHITELIST_USER']
|
||||
else:
|
||||
self.tts_allowed = []
|
||||
self.tts_status = True
|
||||
|
||||
if 'WHITELIST_USER' in conf:
|
||||
self.tts_allowed = conf['WHITELIST_USER']
|
||||
|
||||
def connect(self, server, port, channel, botnick, botpass):
|
||||
logging.info("Connecting to: " + server)
|
||||
try:
|
||||
self.irc.connect((server, port))
|
||||
except ConnectionResetError:
|
||||
logging.fatal('Twitch refused to connect, please check your settings and try again (or just try again, as Twitch sometimes refuses to connect for no reason).')
|
||||
logging.fatal('Twitch refused to connect, please check your settings and try again.')
|
||||
sys.exit(252)
|
||||
|
||||
self.irc.settimeout(1)
|
||||
|
@ -61,7 +79,7 @@ class IRC:
|
|||
sys.exit(255)
|
||||
|
||||
if resp.find('PING') != -1:
|
||||
logging.info('PING received')
|
||||
logging.debug('PING received')
|
||||
self.irc.send(bytes('PONG :tmi.twitch.tv\r\n', "UTF-8"))
|
||||
|
||||
if resp.find('CLEARMSG') != -1:
|
||||
|
@ -125,6 +143,10 @@ class IRC:
|
|||
if msg.startswith('!dtts'):
|
||||
logging.debug("!dtts command detected")
|
||||
user = msg.replace('!dtts', '').strip().lower()
|
||||
|
||||
if user.startswith('@'):
|
||||
logging.debug('Removing "@" from username')
|
||||
user = user.replace('@', '')
|
||||
if user not in self.tts_denied:
|
||||
logging.info("Adding "+str(user)+" to deny list")
|
||||
self.tts_denied.append(user)
|
||||
|
@ -136,6 +158,11 @@ class IRC:
|
|||
logging.debug("!ptts command detected")
|
||||
user = msg.replace('!ptts', '').strip().lower()
|
||||
|
||||
if user.startswith('@'):
|
||||
logging.debug('Removing "@" from username')
|
||||
user = user.replace('@', '')
|
||||
|
||||
logging.info("Adding "+str(user)+" to whitelist")
|
||||
self.tts_allowed.append(user)
|
||||
|
||||
if user in self.tts_denied:
|
||||
|
@ -194,6 +221,10 @@ class IRC:
|
|||
logging.info(self.tts_allowed)
|
||||
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['WHITELISTONLY'])
|
||||
return False
|
||||
else:
|
||||
logging.info('Nobody is on the whitelist.')
|
||||
self.sendpriv(conf['IRC_CHANNEL'], "@"+str(user), conf['MESSAGE']['WHITELISTONLY'])
|
||||
return False
|
||||
|
||||
logging.info('Valid TTS message, adding to raw queue')
|
||||
tts = True
|
||||
|
@ -217,7 +248,15 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
html = fh.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
if self.path == '/tts.js':
|
||||
elif self.path == '/favicon.ico':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'image/x-icon')
|
||||
self.end_headers()
|
||||
with open("favicon.ico", "rb") as fh:
|
||||
icon = fh.read()
|
||||
self.wfile.write(icon)
|
||||
|
||||
elif self.path == '/tts.js':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/javascript')
|
||||
self.end_headers()
|
||||
|
@ -225,7 +264,7 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
html = fh.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
if self.path == '/jquery.js':
|
||||
elif self.path == '/jquery.js':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/javascript')
|
||||
self.end_headers()
|
||||
|
@ -233,7 +272,7 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
html = fh.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
if self.path == '/bootstrap.min.css':
|
||||
elif self.path == '/bootstrap.min.css':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/css')
|
||||
self.end_headers()
|
||||
|
@ -241,7 +280,7 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
html = fh.read()
|
||||
self.wfile.write(html)
|
||||
|
||||
if self.path.startswith('/tts_queue'):
|
||||
elif self.path.startswith('/tts_queue'):
|
||||
tts_json = ""
|
||||
tts = {}
|
||||
self.send_response(200)
|
||||
|
@ -261,7 +300,7 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
tts_json = json.dumps(tts)
|
||||
self.wfile.write(bytes(str(tts_json)+"\n", "utf-8"))
|
||||
|
||||
if self.path.startswith('/tts_done'):
|
||||
elif self.path.startswith('/tts_done'):
|
||||
get_params = parse_qs(self.path)
|
||||
if '/tts_done?id' in get_params:
|
||||
logging.info("Removing message from queue")
|
||||
|
@ -276,6 +315,13 @@ class HTTPserv(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
self.wfile.write(bytes("Internal Server error\n", "utf-8"))
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.send_header('Server', 'TTS')
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes("File not found.\n", "utf-8"))
|
||||
|
||||
return
|
||||
|
||||
def http_serve_forever(httpd):
|
||||
|
@ -309,16 +355,16 @@ def load_config():
|
|||
conf['HTTP_BIND'] = cfg['http']['bind']
|
||||
|
||||
conf['MESSAGE'] = {}
|
||||
conf['MESSAGE']['TOO_LONG'] = cfg['messages']['too_long']
|
||||
conf['MESSAGE']['DISABLED'] = cfg['messages']['disabled']
|
||||
conf['MESSAGE']['DENIED'] = cfg['messages']['denied']
|
||||
conf['MESSAGE']['SUBONLY'] = cfg['messages']['subonly']
|
||||
conf['MESSAGE']['MODONLY'] = cfg['messages']['modonly']
|
||||
conf['MESSAGE']['READY'] = cfg['messages']['ready']
|
||||
conf['MESSAGE']['WHITELISTONLY'] = cfg['messages']['whitelist']
|
||||
conf['MESSAGE']['SAYS'] = cfg['messages']['says']
|
||||
conf['MESSAGE']['TOO_LONG'] = cfg['messages']['too_long'] or "Sorry, your message is too long"
|
||||
conf['MESSAGE']['DISABLED'] = cfg['messages']['disabled'] or "Sorry, TTS is disabled."
|
||||
conf['MESSAGE']['DENIED'] = cfg['messages']['denied'] or "Sorry, you're not allowed to use TTS."
|
||||
conf['MESSAGE']['SUBONLY'] = cfg['messages']['subonly'] or "Sorry, TTS is sub-only."
|
||||
conf['MESSAGE']['MODONLY'] = cfg['messages']['modonly'] or "Sorry, TTS is mod-only."
|
||||
conf['MESSAGE']['READY'] = cfg['messages']['ready'] or "TTS bot is ready."
|
||||
conf['MESSAGE']['WHITELISTONLY'] = cfg['messages']['whitelist'] or False
|
||||
conf['MESSAGE']['SAYS'] = cfg['messages']['says'] or "says"
|
||||
|
||||
conf['USERMAP'] = cfg['usermapping']
|
||||
conf['USERMAP'] = cfg['usermapping'] or []
|
||||
|
||||
if 'whitelist' in cfg:
|
||||
conf['WHITELIST'] = True
|
||||
|
@ -326,7 +372,7 @@ def load_config():
|
|||
else:
|
||||
conf['WHITELIST'] = False
|
||||
|
||||
except KeyError as e:
|
||||
except KeyError:
|
||||
logging.exception('Your config file is invalid, please check and try again.')
|
||||
sys.exit(254)
|
||||
|
||||
|
@ -335,6 +381,15 @@ def load_config():
|
|||
logging.debug('Whitelist:')
|
||||
logging.debug(conf['WHITELIST_USER'])
|
||||
|
||||
if not conf['IRC_CHANNEL']:
|
||||
raise ValueError('Please add your twitch channel to config.yml.')
|
||||
if not conf['IRC_USERNAME']:
|
||||
raise ValueError('Please add the bots username to config.yml.')
|
||||
if not conf['IRC_OAUTH_TOKEN']:
|
||||
raise ValueError('Please add the bots oauth-token to config.yml.')
|
||||
if not conf['IRC_OAUTH_TOKEN'].startswith('oauth:'):
|
||||
raise ValueError('Your oauth-token is invalid, it has to start with: "oauth:"')
|
||||
|
||||
return conf
|
||||
|
||||
conf = {}
|
||||
|
@ -343,11 +398,14 @@ msg_queue_raw = []
|
|||
msg_queue = {}
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(module)s %(threadName)s %(levelname)s: %(message)s')
|
||||
sys.tracebacklimit = 1
|
||||
|
||||
def main():
|
||||
conf = load_config()
|
||||
lastreload = datetime.datetime.now()
|
||||
logging.getLogger().setLevel(conf['LOG_LEVEL'])
|
||||
if conf['LOG_LEVEL'] == 'DEBUG':
|
||||
sys.tracebacklimit = 5
|
||||
|
||||
logging.info("Starting Webserver")
|
||||
httpd = socketserver.TCPServer((conf['HTTP_BIND'], conf['HTTP_PORT']), HTTPserv)
|
||||
|
|
Loading…
Reference in New Issue