wip
This commit is contained in:
parent
b71bedb62a
commit
27cdf52a4a
23 changed files with 5605 additions and 758 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
from kivy_deps import sdl2, glew
|
from kivy_deps import sdl2, glew
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['src\\markovbot_gui\\main.py'],
|
['src\\huesoporro\\main.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[],
|
||||||
|
|
@ -22,7 +22,7 @@ exe = EXE(
|
||||||
a.binaries,
|
a.binaries,
|
||||||
a.datas,
|
a.datas,
|
||||||
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
|
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
|
||||||
name='markovbot',
|
name='huesoporro',
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
|
|
|
||||||
BIN
output.wav
Normal file
BIN
output.wav
Normal file
Binary file not shown.
|
|
@ -1,10 +1,9 @@
|
||||||
[project]
|
[project]
|
||||||
name = "markovbot-gui"
|
name = "huesoporro"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
description = "Markov Chain Bot GUI"
|
description = "Twitch misc bot"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "tomaarsen" },
|
|
||||||
{ name = "185504a9", email = "catalin@roboces.dev" }
|
{ name = "185504a9", email = "catalin@roboces.dev" }
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
@ -18,6 +17,8 @@ dependencies = [
|
||||||
"pyinstaller>=6.11.0",
|
"pyinstaller>=6.11.0",
|
||||||
"twitchwebsocket>=1.2.1",
|
"twitchwebsocket>=1.2.1",
|
||||||
"loguru>=0.7.2",
|
"loguru>=0.7.2",
|
||||||
|
"pyttsx3>=2.98",
|
||||||
|
"tts>=0.22.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ import sqlite3
|
||||||
import string
|
import string
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import platformdirs
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from src.huesoporro.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
"""
|
"""
|
||||||
|
|
@ -86,14 +87,16 @@ class Database:
|
||||||
to both get results from "hello" and "hello,".
|
to both get results from "hello" and "hello,".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, channel: str):
|
def __init__(self, channel: str, settings: Settings | None = None):
|
||||||
self.user_data_path = platformdirs.user_data_path(
|
# self.user_data_path = platformdirs.user_data_path(
|
||||||
"markovbot_gui",
|
# "huesoporro",
|
||||||
ensure_exists=True,
|
# ensure_exists=True,
|
||||||
)
|
# )
|
||||||
self.db_path = (
|
# self.db_path = (
|
||||||
self.user_data_path / f"MarkovChain_{channel.replace('#', '').lower()}.db"
|
# self.user_data_path / f"MarkovChain_{channel.replace('#', '').lower()}.db"
|
||||||
)
|
# )
|
||||||
|
self.s = settings or Settings.read()
|
||||||
|
self.db_path = self.s.path_db
|
||||||
self._execute_queue: list = []
|
self._execute_queue: list = []
|
||||||
|
|
||||||
if self.db_path.is_file():
|
if self.db_path.is_file():
|
||||||
|
|
@ -191,6 +194,7 @@ class Database:
|
||||||
fetch=True,
|
fetch=True,
|
||||||
):
|
):
|
||||||
logger.info("Creating backup before updating Database...")
|
logger.info("Creating backup before updating Database...")
|
||||||
|
|
||||||
# Connect to both the new and backup, backup, and close both
|
# Connect to both the new and backup, backup, and close both
|
||||||
|
|
||||||
def progress(status, remaining, total):
|
def progress(status, remaining, total):
|
||||||
|
|
@ -265,7 +269,7 @@ class Database:
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
self.add_execute_queue(
|
self.add_execute_queue(
|
||||||
f'INSERT INTO MarkovGrammar{first_char}{second_char} SELECT * FROM MarkovGrammar{first_char} WHERE word2 LIKE "{second_char}%";', # noqa: S608
|
f'INSERT INTO MarkovGrammar{first_char}{second_char} SELECT * FROM MarkovGrammar{first_char} WHERE word2 LIKE "{second_char}%";',
|
||||||
)
|
)
|
||||||
self.add_execute_queue(
|
self.add_execute_queue(
|
||||||
f'DELETE FROM MarkovGrammar{first_char} WHERE word2 LIKE "{second_char}%";', # noqa: S608
|
f'DELETE FROM MarkovGrammar{first_char} WHERE word2 LIKE "{second_char}%";', # noqa: S608
|
||||||
|
|
@ -356,7 +360,7 @@ class Database:
|
||||||
|
|
||||||
from nltk import ngrams
|
from nltk import ngrams
|
||||||
|
|
||||||
from src.markovbot_gui.libs.tokenizer import tokenize
|
from src.huesoporro.libs.tokenizer import tokenize
|
||||||
|
|
||||||
channel = channel.replace("#", "").lower()
|
channel = channel.replace("#", "").lower()
|
||||||
copyfile(
|
copyfile(
|
||||||
39
src/huesoporro/domain.py
Normal file
39
src/huesoporro/domain.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import abc
|
||||||
|
from abc import ABC
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import AnyUrl, BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Commands(StrEnum):
|
||||||
|
GENERATE = "!g"
|
||||||
|
BLACKLIST = "!blacklist"
|
||||||
|
GENERATE_HELP = "!ghelp"
|
||||||
|
QUOTE = "!q"
|
||||||
|
QUOTE_ADD = "!qadd"
|
||||||
|
QUOTE_DEL = "!qdel"
|
||||||
|
COPYPASTA_ADD = "!cadd"
|
||||||
|
COPYPASTA_DEL = "!cdel"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatSourceTypes(StrEnum):
|
||||||
|
TWITCH = "twitch"
|
||||||
|
DISCORD = "discord"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatSource(BaseModel, ABC):
|
||||||
|
source: ChatSourceTypes
|
||||||
|
host: AnyUrl
|
||||||
|
port: int
|
||||||
|
authentication: str
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def parse_message(self, message: str) -> str | Commands: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Huesoporro(BaseModel):
|
||||||
|
chat_sources: list[ChatSourceTypes] = Field(
|
||||||
|
default_factory=lambda: [ChatSourceTypes.TWITCH]
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(self): ...
|
||||||
0
src/huesoporro/gui/__init__.py
Normal file
0
src/huesoporro/gui/__init__.py
Normal file
|
|
@ -10,8 +10,8 @@ from kivy.uix.button import Button
|
||||||
from kivy.uix.textinput import TextInput
|
from kivy.uix.textinput import TextInput
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.markovbot_gui.libs.markov_chain_bot import MarkovChain
|
from src.huesoporro.markov_chain_bot import MarkovChain
|
||||||
from src.markovbot_gui.libs.settings import Settings
|
from src.huesoporro.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
class QueueHandler:
|
class QueueHandler:
|
||||||
|
|
@ -8,8 +8,8 @@ from kivy.uix.label import Label
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.textinput import TextInput
|
from kivy.uix.textinput import TextInput
|
||||||
|
|
||||||
from src.markovbot_gui.libs.settings import Settings
|
from src.huesoporro.settings import Settings
|
||||||
from src.markovbot_gui.libs.timer import logger
|
from src.huesoporro.timer import logger
|
||||||
|
|
||||||
|
|
||||||
class ConfigWindow(BoxLayout):
|
class ConfigWindow(BoxLayout):
|
||||||
|
|
@ -6,17 +6,15 @@ from kivy.uix.button import Button
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.widget import Widget
|
from kivy.uix.widget import Widget
|
||||||
|
|
||||||
from src.markovbot_gui.bot_runner import BotRunner
|
from src.huesoporro.gui.bot_runner import BotRunner
|
||||||
from src.markovbot_gui.config_window import ConfigWindow
|
from src.huesoporro.gui.config_window import ConfigWindow
|
||||||
|
|
||||||
|
|
||||||
class BotApp(App):
|
class BotApp(App):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.config_path = (
|
self.config_path = platformdirs.user_config_path("huesoporro") / "settings.json"
|
||||||
platformdirs.user_config_path("markovbot_gui") / "settings.json"
|
self.data_path = platformdirs.user_data_path("huesoporro")
|
||||||
)
|
|
||||||
self.data_path = platformdirs.user_data_path("markovbot_gui")
|
|
||||||
|
|
||||||
def run_bot(self, instance):
|
def run_bot(self, instance):
|
||||||
bot_runner = BotRunner(settings_path=self.config_path)
|
bot_runner = BotRunner(settings_path=self.config_path)
|
||||||
0
src/huesoporro/markov/__init__.py
Normal file
0
src/huesoporro/markov/__init__.py
Normal file
|
|
@ -6,10 +6,10 @@ from loguru import logger
|
||||||
from nltk.tokenize import sent_tokenize
|
from nltk.tokenize import sent_tokenize
|
||||||
from TwitchWebsocket import Message, TwitchWebsocket
|
from TwitchWebsocket import Message, TwitchWebsocket
|
||||||
|
|
||||||
from src.markovbot_gui.libs.db import Database
|
from src.huesoporro.db import Database
|
||||||
from src.markovbot_gui.libs.settings import Settings
|
from src.huesoporro.settings import Settings
|
||||||
from src.markovbot_gui.libs.timer import LoopingTimer
|
from src.huesoporro.timer import LoopingTimer
|
||||||
from src.markovbot_gui.libs.tokenizer import detokenize, tokenize
|
from src.huesoporro.tokenizer import detokenize, tokenize
|
||||||
|
|
||||||
|
|
||||||
class Commands(StrEnum):
|
class Commands(StrEnum):
|
||||||
|
|
@ -297,7 +297,7 @@ class MarkovChain:
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
logger.info("Bot not enabled, skipping")
|
logger.info("Bot not enabled, skipping")
|
||||||
return
|
return
|
||||||
if message.user not in self.s.denied_users:
|
if message.user not in [*self.s.denied_users, self.s.channel_name]:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {message.user} allowed to generate, executing _command_generate()",
|
f"User {message.user} allowed to generate, executing _command_generate()",
|
||||||
)
|
)
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import ClassVar, Literal
|
||||||
|
|
||||||
import platformdirs
|
import platformdirs
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import Field
|
from pydantic import DirectoryPath, Field, FilePath
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ class Settings(BaseSettings):
|
||||||
serialization_alias="Mods",
|
serialization_alias="Mods",
|
||||||
)
|
)
|
||||||
cooldown: int = Field(210, alias="Cooldown", serialization_alias="Cooldown")
|
cooldown: int = Field(210, alias="Cooldown", serialization_alias="Cooldown")
|
||||||
key_length: int = Field(2, alias="KeyLength", serialization_alias="KeyLength")
|
key_length: int = Field(4, alias="KeyLength", serialization_alias="KeyLength")
|
||||||
max_sentence_length: int = Field(
|
max_sentence_length: int = Field(
|
||||||
25,
|
25,
|
||||||
alias="MaxSentenceWordAmount",
|
alias="MaxSentenceWordAmount",
|
||||||
|
|
@ -88,30 +88,35 @@ class Settings(BaseSettings):
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
"TRACE",
|
"TRACE",
|
||||||
] = Field("DEBUG", alias="LogLevel")
|
] = Field("DEBUG", alias="LogLevel")
|
||||||
|
|
||||||
|
path_settings: ClassVar[FilePath] = (
|
||||||
|
platformdirs.user_config_path("huesoporro", ensure_exists=True)
|
||||||
|
/ "settings.json"
|
||||||
|
)
|
||||||
|
path_data: ClassVar[DirectoryPath] = platformdirs.user_data_path(
|
||||||
|
"huesoporro", ensure_exists=True
|
||||||
|
)
|
||||||
|
|
||||||
model_config = SettingsConfigDict(extra="ignore")
|
model_config = SettingsConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_db(self) -> FilePath:
|
||||||
|
return self.path_data / f"MarkovChain_{self.channel_name}.db"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel_name(self):
|
def channel_name(self):
|
||||||
return self.channel.replace("#", "").lower()
|
return self.channel.replace("#", "").lower()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, filepath: Path | None = None) -> "Settings":
|
def read(cls, filepath: Path | None = None) -> "Settings":
|
||||||
if not filepath:
|
filepath = filepath or cls.path_settings
|
||||||
filepath = (
|
|
||||||
platformdirs.user_config_path("markovbot_gui", ensure_exists=True)
|
|
||||||
/ "settings.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
with filepath.open("r") as f:
|
with filepath.open("r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return Settings(**data)
|
return Settings(**data)
|
||||||
|
|
||||||
def write(self, filepath: Path | None = None):
|
def write(self, filepath: Path | None = None):
|
||||||
if not filepath:
|
filepath = filepath or self.path_settings
|
||||||
filepath = (
|
|
||||||
platformdirs.user_config_path("markovbot_gui", ensure_exists=True)
|
|
||||||
/ "settings.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
with filepath.open("w") as f:
|
with filepath.open("w") as f:
|
||||||
logger.info(f"Writing current settings to {filepath}")
|
logger.info(f"Writing current settings to {filepath}")
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2019 CubieDev
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
# TwitchMarkovChain
|
|
||||||
|
|
||||||
Twitch Bot for generating messages based on what it learned from chat
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
When the bot has started, it will start listening to chat messages in the channel listed in the `settings.json` file. Any chat message not sent by a denied user will be learned from. Whenever someone then requests a message to be generated, a [Markov Chain](https://en.wikipedia.org/wiki/Markov_chain) will be used with the learned data to generate a sentence. **Note that the bot is unaware of the meaning of any of its inputs and outputs. This means it can use bad language if it was taught to use bad language by people in chat. You can add a list of banned words it should never learn or say. Use at your own risk.**
|
|
||||||
|
|
||||||
Whenever a message is deleted from chat, it's contents will be unlearned at 5 times the rate a normal message is learned from.
|
|
||||||
The bot will avoid learning from commands, or from messages containing links.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
### Sentence Parsing
|
|
||||||
|
|
||||||
To explain how the bot works, I will provide an example situation with two messages that are posted in Twitch chat. The messages are:
|
|
||||||
|
|
||||||
> Curly fries are the worst kind of fries
|
|
||||||
> Loud people are the reason I don't go to the movies anymore
|
|
||||||
|
|
||||||
Let's start with the first sentence and parse it like the bot will. To do so, we will split up the sentence in sections of `keyLength + 1` words. As `keyLength` has been set to `2` in the [Settings](#settings) section, each section has `3` words.
|
|
||||||
|
|
||||||
```txt
|
|
||||||
Curly fries are the worst kind of fries
|
|
||||||
[Curly fries:are]
|
|
||||||
[fries are:the]
|
|
||||||
[are the:worst]
|
|
||||||
[the worst:kind]
|
|
||||||
[worst kind:of]
|
|
||||||
[kind of:fries]
|
|
||||||
```
|
|
||||||
|
|
||||||
For each of these sections of three words, the last word is considered the output, while all other words it are considered inputs.
|
|
||||||
These words are then turned into a variation of a [Grammar](https://en.wikipedia.org/wiki/Formal_grammar):
|
|
||||||
|
|
||||||
```txt
|
|
||||||
"Curly fries" -> "are"
|
|
||||||
"fries are" -> "the"
|
|
||||||
"are the" -> "worst"
|
|
||||||
"the worst" -> "kind"
|
|
||||||
"worst kind" -> "of"
|
|
||||||
"kind of" -> "fries"
|
|
||||||
```
|
|
||||||
|
|
||||||
This can be considered a mathematical function that, when given input "the worst", will output "kind".
|
|
||||||
In order for the program to know where sentences begin, we also add the first `keyLength` words to a seperate Database table, where a list of possible starts of sentences reside.
|
|
||||||
|
|
||||||
This exact same process is applied to the second sentence as well. After doing so, the resulting grammar (and our corresponding database table) looks like:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
"Curly fries" -> "are"
|
|
||||||
"fries are" -> "the"
|
|
||||||
"are the" -> "worst" | "reason"
|
|
||||||
"the worst" -> "kind"
|
|
||||||
"worst kind" -> "of"
|
|
||||||
"kind of" -> "fries"
|
|
||||||
"Loud people" -> "are"
|
|
||||||
"people are" -> "the"
|
|
||||||
"the reason" -> "I"
|
|
||||||
"reason I" -> "don't"
|
|
||||||
"I don't" -> "go"
|
|
||||||
"don't go" -> "to"
|
|
||||||
"go to" -> "the"
|
|
||||||
"to the" -> "movies"
|
|
||||||
"the movies" -> "anymore"
|
|
||||||
```
|
|
||||||
|
|
||||||
and in the database table for starts of sentences:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
"Curly fries"
|
|
||||||
"Loud people"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the | is considered to be _"or"_. In the case of the bold text above, it could be read as: if the given input is "are the", then the output is either _"worst"_ **or** _"reason"_.
|
|
||||||
|
|
||||||
In practice, more frequent phrases will have higher precedence. The more often a phrase is said, the more likely it is to be generated.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Generation
|
|
||||||
|
|
||||||
When a message is generated with `!generate`, a random start of a sentence is picked from the database table of starts of sentences. In our example the randomly picked start is _"Curly fries"_.
|
|
||||||
|
|
||||||
Now, in a loop:
|
|
||||||
|
|
||||||
- The output for the input is generated via the grammar.
|
|
||||||
- And the input for the next iteration in the loop is shifted:
|
|
||||||
- Remove the first word from the input.
|
|
||||||
- Add the new output word to the end of the input.
|
|
||||||
|
|
||||||
So, the input starts as _"Curly Fries"_. The output for this input is generated via the grammar, which gives us _"are"_. Then, the input is updated. _"Curly"_ is removed, and _"are"_ is added to the input. The new input for the next iteration will be _"Fries are"_ as a result. This process repeats until no more words can be generated, or if a word limit is reached.
|
|
||||||
|
|
||||||
A more programmatic example of this would be this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# This initial sentence is either from the database for starts of sentences,
|
|
||||||
# or from words passed in Twitch chat
|
|
||||||
sentence = ["Curly", "fries"]
|
|
||||||
for i in range(sentence_length):
|
|
||||||
# Generate a word using last 2 words in the partial sentence,
|
|
||||||
# and append it to the partial sentence
|
|
||||||
sentence.append(generate(sentence[-2:]))
|
|
||||||
```
|
|
||||||
|
|
||||||
It's common for an input sequence to have multiple possible outputs, as we can see in the bold part of the previous grammar. This allows learned information from multiple messages to be merged into one message. For instance, some potential outputs from the given example are
|
|
||||||
|
|
||||||
> Curly fries are the reason I don't go to the movies anymore
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
> Loud people are the worst kind of fries
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
Chat members can generate chat-like messages using the following commands (Note that they are aliases):
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!generate [words]
|
|
||||||
!g [words]
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!g Curly
|
|
||||||
```
|
|
||||||
|
|
||||||
Result (for example):
|
|
||||||
|
|
||||||
```txt
|
|
||||||
Curly fries are the reason I don't go to the movies anymore
|
|
||||||
```
|
|
||||||
|
|
||||||
- The bot will, when given this command, try to complete the start of the sentence which was given.
|
|
||||||
- If it cannot, an appropriate error message will be sent to chat.
|
|
||||||
- Any number of words may be given, including none at all.
|
|
||||||
- Everyone can use it.
|
|
||||||
|
|
||||||
Furthermore, chat members can find a link to [How it works](#how-it-works) by using one of the following commands:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!ghelp
|
|
||||||
!genhelp
|
|
||||||
!generatehelp
|
|
||||||
```
|
|
||||||
|
|
||||||
The use of this command makes the bot post this message in chat:
|
|
||||||
|
|
||||||
> Learn how this bot generates sentences here: <https://github.com/CubieDev/TwitchMarkovChain#how-it-works>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Streamer commands
|
|
||||||
|
|
||||||
All of these commands can be whispered to the bot account, or typed in chat.
|
|
||||||
To disable the bot from generating messages, while still learning from regular chat messages:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!disable
|
|
||||||
```
|
|
||||||
|
|
||||||
After disabling the bot, it can be re-enabled using:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!enable
|
|
||||||
```
|
|
||||||
|
|
||||||
Changing the cooldown between generations is possible with one of the following two commands:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!setcooldown <seconds>
|
|
||||||
!setcd <seconds>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!setcd 30
|
|
||||||
```
|
|
||||||
|
|
||||||
Which sets the cooldown between generations to 30 seconds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Moderator commands
|
|
||||||
|
|
||||||
All of these commands must be whispered to the bot account.
|
|
||||||
Moderators (and the broadcaster) can modify the blacklist to prevent the bot learning words it shouldn't.
|
|
||||||
To add `word` to the blacklist, a moderator can whisper the bot:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!blacklist <word>
|
|
||||||
```
|
|
||||||
|
|
||||||
Similarly, to remove `word` from the blacklist, a moderator can whisper the bot:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!whitelist <word>
|
|
||||||
```
|
|
||||||
|
|
||||||
And to check whether `word` is already on the blacklist or not, a moderator can whisper the bot:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
!check <word>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
This bot is controlled by a `settings.json` file, which has the following structure:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Host": "irc.chat.twitch.tv",
|
|
||||||
"Port": 6667,
|
|
||||||
"Channel": "#<channel>",
|
|
||||||
"Nickname": "<name>",
|
|
||||||
"Authentication": "oauth:<auth>",
|
|
||||||
"DeniedUsers": ["StreamElements", "Nightbot", "Moobot", "Marbiebot"],
|
|
||||||
"AllowedUsers": [],
|
|
||||||
"Cooldown": 20,
|
|
||||||
"KeyLength": 2,
|
|
||||||
"MaxSentenceWordAmount": 25,
|
|
||||||
"MinSentenceWordAmount": -1,
|
|
||||||
"HelpMessageTimer": 18000,
|
|
||||||
"AutomaticGenerationTimer": -1,
|
|
||||||
"WhisperCooldown": true,
|
|
||||||
"EnableGenerateCommand": true,
|
|
||||||
"SentenceSeparator": " - ",
|
|
||||||
"AllowGenerateParams": true,
|
|
||||||
"GenerateCommands": ["!generate", "!g"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| **Parameter** | **Meaning** | **Example** |
|
|
||||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
||||||
| `Host` | The URL that will be used. Do not change. | `"irc.chat.twitch.tv"` |
|
|
||||||
| `Port` | The Port that will be used. Do not change. | `6667` |
|
|
||||||
| `Channel` | The Channel that will be connected to. | `"#CubieDev"` |
|
|
||||||
| `Nickname` | The Username of the bot account. | `"CubieB0T"` |
|
|
||||||
| `Authentication` | The OAuth token for the bot account. | `"oauth:pivogip8ybletucqdz4pkhag6itbax"` |
|
|
||||||
| `DeniedUsers` | The list of (bot) accounts whose messages should not be learned from. The bot itself it automatically added to this. | `["StreamElements", "Nightbot", "Moobot", "Marbiebot"]` |
|
|
||||||
| `AllowedUsers` | A list of users with heightened permissions. Gives these users the same power as the channel owner, allowing them to bypass cooldowns, set cooldowns, disable or enable the bot, etc. | `["Michelle", "Cubie"]` |
|
|
||||||
| `Cooldown` | A cooldown in seconds between successful generations. If a generation fails (eg inputs it can't work with), then the cooldown is not reset and another generation can be done immediately. | `20` |
|
|
||||||
| `KeyLength` | A technical parameter which, in my previous implementation, would affect how closely the output matches the learned inputs. In the current implementation the database structure does not allow this parameter to be changed. Do not change. | `2` |
|
|
||||||
| `MaxSentenceWordAmount` | The maximum number of words that can be generated. Prevents absurdly long and spammy generations. | `25` |
|
|
||||||
| `MinSentenceWordAmount` | The minimum number of words that can be generated. Might generate multiple sentences, separated by the value from `SentenceSeparator`. Prevents very short generations. -1 to disable. | `-1` |
|
|
||||||
| `HelpMessageTimer` | The amount of seconds between sending help messages that links to [How it works](#how-it-works). -1 for no help messages. Defaults to once every 5 hours. | `18000` |
|
|
||||||
| `AutomaticGenerationTimer` | The amount of seconds between automatically sending a generated message, as if someone wrote `!g`. -1 for no automatic generations. | `-1` |
|
|
||||||
| `WhisperCooldown` | Allows the bot to whisper a user the remaining cooldown after that user has attempted to generate a message. | `true` |
|
|
||||||
| `EnableGenerateCommand` | Globally enables/disables the generate command. | `true` |
|
|
||||||
| `SentenceSeparator` | The separator between multiple sentences. Only relevant if `MinSentenceWordAmount` > 0, as only then can multiple sentences be generated. Sensible values for this might be `", "`, `". "`, `" - "` or `" "`. | `" - "` |
|
|
||||||
| `AllowGenerateParams` | Allow chat to supply a partial sentence which the bot finishes, e.g. `!generate hello, I am`. If `false`, all values after the generation command will be ignored. | `true` |
|
|
||||||
| `GenerateCommands` | The generation commands that the bot will listen for. Defaults to `["!generate", "!g"]`. Useful if your chat is used to commands with `~`, `-`, `/`, etc. | `["!generate", "!g"]` |
|
|
||||||
|
|
||||||
_Note that the example OAuth token is not an actual token, but merely a generated string to give an indication what it might look like._
|
|
||||||
|
|
||||||
I got my real OAuth token from <https://twitchapps.com/tmi/>.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Blacklist
|
|
||||||
|
|
||||||
You may add words to a blacklist by adding them on a separate line in `blacklist.txt`. Each word is case insensitive. By default, this file only contains `<start>` and `<end>`, which are required for the current implementation.
|
|
||||||
|
|
||||||
Words can also be added or removed from the blacklist via whispers, as is described in the [Moderator Command](#moderator-commands) section.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- [Python 3.6+](https://www.python.org/downloads/)
|
|
||||||
- [Module requirements](requirements.txt)
|
|
||||||
- Install these modules using `pip install -r requirements.txt` in the commandline.
|
|
||||||
|
|
||||||
Among these modules is my own [TwitchWebsocket](https://github.com/tomaarsen/TwitchWebsocket) wrapper, which makes making a Twitch chat bot a lot easier.
|
|
||||||
This repository can be seen as an implementation using this wrapper.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Contributors
|
|
||||||
My gratitude is extended to the following contributors who've decided to help out.
|
|
||||||
* [@DoctorInsano](https://github.com/DoctorInsano) - Several small fixes and improvements in [v1.0](https://github.com/tomaarsen/TwitchMarkovChain/releases/tag/v1.0).
|
|
||||||
* [@justinrusso](https://github.com/justinrusso) - Several features, refactors and fixes, that represent the core of [v2.0](https://github.com/tomaarsen/TwitchMarkovChain/releases/tag/v2.0) and [v2.1](https://github.com/tomaarsen/TwitchMarkovChain/releases/tag/v2.1).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Other Twitch Bots
|
|
||||||
|
|
||||||
- [TwitchAIDungeon](https://github.com/CubieDev/TwitchAIDungeon)
|
|
||||||
- [TwitchGoogleTranslate](https://github.com/CubieDev/TwitchGoogleTranslate)
|
|
||||||
- [TwitchCubieBotGUI](https://github.com/CubieDev/TwitchCubieBotGUI)
|
|
||||||
- [TwitchCubieBot](https://github.com/CubieDev/TwitchCubieBot)
|
|
||||||
- [TwitchRandomRecipe](https://github.com/CubieDev/TwitchRandomRecipe)
|
|
||||||
- [TwitchUrbanDictionary](https://github.com/CubieDev/TwitchUrbanDictionary)
|
|
||||||
- [TwitchRhymeBot](https://github.com/CubieDev/TwitchRhymeBot)
|
|
||||||
- [TwitchWeather](https://github.com/CubieDev/TwitchWeather)
|
|
||||||
- [TwitchDeathCounter](https://github.com/CubieDev/TwitchDeathCounter)
|
|
||||||
- [TwitchSuggestDinner](https://github.com/CubieDev/TwitchSuggestDinner)
|
|
||||||
- [TwitchPickUser](https://github.com/CubieDev/TwitchPickUser)
|
|
||||||
- [TwitchSaveMessages](https://github.com/CubieDev/TwitchSaveMessages)
|
|
||||||
- [TwitchMMLevelPickerGUI](https://github.com/CubieDev/TwitchMMLevelPickerGUI) (Mario Maker 2 specific bot)
|
|
||||||
- [TwitchMMLevelQueueGUI](https://github.com/CubieDev/TwitchMMLevelQueueGUI) (Mario Maker 2 specific bot)
|
|
||||||
- [TwitchPackCounter](https://github.com/CubieDev/TwitchPackCounter) (Streamer specific bot)
|
|
||||||
- [TwitchDialCheck](https://github.com/CubieDev/TwitchDialCheck) (Streamer specific bot)
|
|
||||||
- [TwitchSendMessage](https://github.com/CubieDev/TwitchSendMessage) (Meant for debugging purposes)
|
|
||||||
22
test.py
Normal file
22
test.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import torch
|
||||||
|
from TTS.api import TTS
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
print(TTS().list_models().list_models())
|
||||||
|
|
||||||
|
tts = TTS(
|
||||||
|
model_name="tts_models/multilingual/multi-dataset/xtts_v2", progress_bar=False
|
||||||
|
).to(device)
|
||||||
|
|
||||||
|
tts.tts_to_file(
|
||||||
|
"Esto es una prueba. Esto es otra prueba!!",
|
||||||
|
speaker="Gracie Wise",
|
||||||
|
language="es",
|
||||||
|
file_path="output.wav",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
14
tests/test_unit.py
Normal file
14
tests/test_unit.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
def test_start():
|
||||||
|
bot = Huesoporro(chat_sources=[ChatSources.TWITCH, ChatSources.DISCORD])
|
||||||
|
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate():
|
||||||
|
mkv = MarkovChain()
|
||||||
|
|
||||||
|
inputless_generation = mkv.generate()
|
||||||
|
|
||||||
|
fries_generation = mkv.generate("fries")
|
||||||
|
|
||||||
|
curly_fries_generation = mkv.generate("curly fries")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue