Compare commits

..

21 commits
2.5.0 ... main

Author SHA1 Message Date
d423d668e7 Merge pull request 'develop' (#7) from develop into main
Reviewed-on: #7
2024-10-13 22:45:00 +02:00
Khyretos
07462ac5af Merge branch 'develop' of https://git.kreative-kompas.com/khyretos/LoquendoBot into develop 2024-10-13 22:41:30 +02:00
Khyretos
cc38338b15 test 2024-10-13 22:37:36 +02:00
Khyretos
a9f824a518 test commit 2024-10-13 22:25:57 +02:00
Khyretos
da985d3e42 Lots of changes 2024-10-13 22:21:59 +02:00
d4f7af6161 test 2024-10-08 07:03:51 +02:00
bc7beab074
Merge pull request #5 from Khyretos/develop
Develop
2024-01-05 04:34:57 +01:00
Khyretos
8a96908c48 menu bugfix. 2024-01-05 04:33:58 +01:00
Khyretos
7c135e293b version bump. 2024-01-05 04:21:49 +01:00
Khyretos
8eb0bf491a gitignore update 2024-01-05 04:15:06 +01:00
Khyretos
ccda0a7736 betterttv, send translated message. 2024-01-05 04:14:49 +01:00
Khyretos
0efb495339 Language fixes 2024-01-04 21:58:15 +01:00
Khyretos
932a21647d new language selector (WIP) 2024-01-04 06:14:51 +01:00
Khyretos
2f41de4fdd version bump 2024-01-03 15:10:57 +01:00
Khyretos
d83c0ad753 language detection fixes and installer change. 2024-01-03 14:26:08 +01:00
Khyretos
5cfdd498c9 version bump 2023-12-31 22:13:44 +01:00
Khyretos
bfd94da1d0 extra changes 2023-12-31 22:13:19 +01:00
Khyretos
f1df6b24ed language detection, twitch funcitonality updates 2023-12-31 22:12:52 +01:00
Khyretos
f7e3248b90 twtitch emote support 2023-12-29 06:17:18 +01:00
Khyretos
525cb6116e various updates. 2023-12-28 16:25:52 +01:00
Khyretos
914cf831c4 fixed user message placement and some css cleanup 2023-12-26 14:02:24 +01:00
353 changed files with 51421 additions and 5657 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

5
.eslintignore Normal file
View file

@ -0,0 +1,5 @@
node_modules
docs
dist
out
build

16
.eslintrc.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = {
root: true,
env: {
es6: true,
node: true
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021
},
extends: ['eslint:recommended', '@electron-internal', '@electron-toolkit'],
rules: {
'space-before-function-paren': 'off',
vendorPrefix: 'off'
}
};

8
.gitignore vendored
View file

@ -104,4 +104,10 @@ src/sounds/tts/*
loquendoBot_backend.spec
forge.config.js
backend/*
src/backend/loquendoBot_backend.exe
!backend/loquendoBot_backend.py
backend/loquendoBot_backend.exe
src/config/twitch-emotes.json
dist/*
src/config/betterttv-emotes.json
test.py
src/config/settings.json

5
.prettierrc.yaml Normal file
View file

@ -0,0 +1,5 @@
singleQuote: true
semi: true
printWidth: 140
trailingComma: none
arrowParens: avoid

View file

@ -1,10 +1,17 @@
from flask import Flask, Response, jsonify, request
import gevent
import re
import gevent.monkey
import json
from waitress import serve
import logging
logger = logging.getLogger("waitress")
logger.setLevel(logging.INFO)
gevent.monkey.patch_all()
import gevent.queue
# import gevent.queue
import configparser
import pyttsx3
@ -21,25 +28,29 @@ from deep_translator import (
MyMemoryTranslator,
)
import emoji
from vosk import Model, KaldiRecognizer, SetLogLevel
# global variables
SetLogLevel(-1)
settings = configparser.ConfigParser()
settings = None;
app = Flask(__name__)
if len(sys.argv) > 1:
settingsPath = os.path.normpath(sys.argv[1])
environment = sys.argv[2]
q = queue.Queue()
# gobal functions
def loadSettings():
with open(settingsPath, 'r') as file:
global settings
settings = json.load(file)
# classes
class LanguageDetection:
def __init__(self):
@ -56,19 +67,18 @@ class LanguageDetection:
resources_folder, "language_detection_model", f"lid.176.bin"
)
language_detection_model = (
rf"{language_detection_model}"
)
language_detection_model = rf"{language_detection_model}"
self.model = fasttext.load_model(language_detection_model)
def predict_lang(self, text):
predictions = self.model.predict(text, k=5) # returns top 2 matching languages
predictions = self.model.predict(text, k=3) # returns top 2 matching languages
language_codes = []
for prediction in predictions[0]:
language_codes.append(prediction.replace("__label__", ""))
return language_codes
class STT:
samplerate = None
args = ""
@ -92,9 +102,7 @@ class STT:
resources_folder, "speech_to_text_models", settings["STT"]["LANGUAGE"]
)
self.model = Model(
rf"{vosk_model}"
)
self.model = Model(rf"{vosk_model}")
self.dump_fn = None
self.q = gevent.queue.Queue()
@ -132,8 +140,9 @@ class STT:
def stop_recognition(self):
self.is_running = False
speech_recognition_service = STT()
loadSettings()
if settings["STT"]["USE_STT"] and settings["STT"]["LANGUAGE"] != '':
speech_recognition_service = STT()
class TTS:
@ -151,16 +160,16 @@ class TTS:
break
self.engine.setProperty("voice", matching_id)
if environment == "dev":
settings_folder = os.path.dirname(settingsPath)
if environment == "dev":
src_folder = os.path.dirname(settings_folder)
bot_folder = os.path.dirname(src_folder)
saveLocation = os.path.join(
src_folder, "sounds\\tts", f"Internal_{count}.mp3"
bot_folder, "sounds", f"Internal_{count}.mp3"
)
else:
resources_folder = os.path.dirname(settingsPath)
saveLocation = os.path.join(
resources_folder, "sounds\\tts", f"Internal_{count}.mp3"
settings_folder, "sounds", f"Internal_{count}.mp3"
)
self.engine.save_to_file(message, saveLocation)
@ -175,11 +184,13 @@ class TTS:
return [voice.name for voice in voices]
text_to_speech_service = TTS()
loadSettings()
if settings["TTS"]["USE_TTS"]:
text_to_speech_service = TTS()
# endpoints
@app.route("/stream", methods=["GET"])
def stream_recognition():
def generate():
@ -194,14 +205,6 @@ def stop_recording():
return Response("Speech recognition stopped", status=200)
# @app.before_request
# def custom_warning():
# if environment == "dev":
# print(
# # "Running in internal development environment. This server is not for production use."
# )
@app.route("/terminate", methods=["GET"])
def terminate_processes():
shutdown_server()
@ -215,35 +218,55 @@ def shutdown_server():
func()
# @app.route("/detect", methods=["POST"])
# def server_status():
# try:
# request_data = request.json
# message = request_data.get("message", "")
# confidence_values = detector.compute_language_confidence_values(message)
# for language, value in confidence_values:
# print(f"{language.name}: {value:.2f}")
# message = request_data.get("message", "")
# except Exception as e:
# return jsonify({"error": "An error occurred"}), 500
# return jsonify({"message": "Audio triggered"}), 200
@app.route("/status", methods=["GET"])
def server_status():
return jsonify({"status": "server is running"})
@app.route("/detect", methods=["POST"])
def get_language():
try:
request_data = request.json
message = request_data.get("message", "")
lang = LanguageDetection().predict_lang(message)
except Exception as e:
return jsonify({"error": "An error occurred"}), 500
return jsonify({"languages": lang}), 200
@app.route("/translate", methods=["POST"])
def get_translation():
loadSettings()
request_data = request.json
message = request_data.get("message", "")
detectedLanguage = request_data.get("language", "")
try:
# try:
translated = MyMemoryTranslator(
source=detectedLanguage, target=settings["LANGUAGE"]["TRANSLATE_TO"]
).translate(message)
# except Exception as e:
# return jsonify({"error": str(e), "code":429 }), 429
except Exception as e:
return jsonify({"error": str(e), "code":500 }), 500
return jsonify({"translation": translated}), 200
@app.route("/audio", methods=["POST"])
def trigger_backend_event():
try:
request_data = request.json
message = request_data.get("message", "")
filteredMessage = re.sub(
r"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)",
"a link",
message,
)
voice = request_data.get("voice")
count = request_data.get("count")
text_to_speech_service.say(message, voice, count)
text_to_speech_service.say(filteredMessage, voice, count)
except Exception as e:
return jsonify({"error": "An error occurred"}), 500
return jsonify({"error": e}), 500
return jsonify({"message": "Audio triggered"}), 200
@ -253,25 +276,16 @@ def get_voices():
voices = text_to_speech_service.voices()
return jsonify({"voices": voices}), 200
except Exception as e:
return jsonify({"error": "An error occurred"}), 500
return jsonify({"error": e}), 500
if __name__ == "__main__":
LANGUAGE = LanguageDetection()
lang = LANGUAGE.predict_lang("hola cómo estás")
print(lang)
text = "Keep it up. You are awesome"
translated = MyMemoryTranslator(
source="english", target="spanish latin america"
).translate(text)
print(translated)
if len(sys.argv) > 1:
settings.read(settingsPath)
loadSettings()
port = int(settings["GENERAL"]["PORT"])
else:
environment = "dev"
port = 9000
stream_recognition()
app.run(host="127.0.0.1", port=port)
app.terminate()
serve(app, host="0.0.0.0", port=port)

View file

@ -1,36 +0,0 @@
module.exports = {
packagerConfig: {
icon: './src/images/icon.ico',
asar: true,
extraResource: ['./src/config/loquendo.db', './src/sounds', './backend', './language_detection_model', './speech_to_text_models'],
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
setupIcon: './src/images/icon.ico',
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {
options: {},
},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
],
};

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Khyretis
Copyright (c) 2021 Khyretos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,14 +1,37 @@
{
"name": "loquendo-bot",
"version": "2.4.0",
"productName": "LoquendoBot",
"version": "2.6.0",
"description": "Bot assistant for streamers over different platforms",
"main": "src/main.js",
"scripts": {
"start": "electron-forge start",
"package": "npm run backend && electron-forge package",
"make": "electron-forge make",
"build": "npm run backend && electron-builder",
"publish": "electron-forge publish",
"backend": "pyinstaller --noconsole --onefile --collect-all vosk --distpath ./backend ./src/backend/loquendoBot_backend.py"
"backend": "pyinstaller --noconsole --onefile --collect-all vosk --distpath ./backend ./backend/loquendoBot_backend.py"
},
"build": {
"appId": "LoquendoBot",
"win": {
"target": [
"nsis"
],
"icon": "./src/images/icon.ico"
},
"nsis": {
"oneClick": false,
"installerIcon": "./src/images/icon.ico",
"uninstallerIcon": "./src/images/icon.ico",
"uninstallDisplayName": "LoquendoBot-uninstaller",
"license": "license.md",
"allowToChangeInstallationDirectory": "true"
},
"extraResources": [
"speech_to_text_models/Where to get STT models.txt",
"backend/loquendoBot_backend.exe",
"language_detection_model",
"sounds"
]
},
"keywords": [],
"author": {
@ -17,27 +40,39 @@
},
"license": "ISC",
"dependencies": {
"@mediapipe/tasks-vision": "^0.10.12",
"axios": "^1.4.0",
"electron-squirrel-startup": "^1.0.0",
"dlivetv-api": "^1.0.10",
"emoji-picker-element": "^1.21.0",
"express": "^4.18.2",
"flag-icons": "^7.1.0",
"ini": "^2.0.0",
"kill-process-by-name": "^1.0.5",
"node-google-tts-api": "^1.1.1",
"p5": "^1.9.2",
"querystring": "^0.2.1",
"socket.io": "^4.7.1",
"socket.io-client": "^4.7.1",
"sockette": "^2.0.6",
"tmi.js": "^1.8.5",
"url": "^0.11.1",
"winston": "^3.10.0",
"write-ini-file": "^4.0.1"
"write-ini-file": "^4.0.1",
"youtube-chat": "^2.2.0"
},
"devDependencies": {
"@electron-forge/cli": "^6.2.1",
"@electron-forge/maker-deb": "^6.2.1",
"@electron-forge/maker-rpm": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
"electron": "^25.9.8"
"@electron-internal/eslint-config": "^1.0.1",
"@electron-toolkit/eslint-config": "^1.0.2",
"electron": "^25.9.8",
"electron-builder": "^24.9.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"prettier": "^3.1.1"
}
}

View file

@ -45,5 +45,6 @@ After getting your credentials you can set it in <img src="https://raw.githubuse
* [Speech2Go](https://harposoftware.com/en/spanish-spain-/340-S2G-Jorge-Nuance-Voice.html).
### Linux
* WIP
### Mac
* WIP

View file

@ -1 +1,3 @@
https://alphacephei.com/vosk/models
Download the model from here: https://alphacephei.com/vosk/models unzip it
and drop the folder in the 'speech_to_text_models' folder. Restart the app
to load the changes.

View file

@ -1,5 +1,6 @@
@font-face {
font-family: 'FRAMDCN';
src: url(../fonts/FRAMCDN/FRAMDCN.woff);
}
h1 {
@ -14,7 +15,8 @@ h1 {
align-items: center;
flex-direction: column;
background-color: var(--mid-section);
margin-left: 50px;
padding-left: 50px;
padding-right: 50px;
font-family: 'FRAMDCN';
position: relative;
z-index: 1;
@ -160,11 +162,15 @@ h1 {
}
.chat-input input[good] + button {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
box-shadow:
0 0 2px rgba(0, 0, 0, 0.12),
0 2px 4px rgba(0, 0, 0, 0.24);
}
.chat-input input[good] + button:hover {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow:
0 8px 17px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
/* filter: brightness(150%); */
}
@ -175,40 +181,47 @@ h1 {
.msg-container {
direction: ltr;
position: static;
display: inline-block;
width: 100%;
padding: 0px 0px 10px 0px;
padding: 10px 0px 0px 0px;
display: grid;
grid-template: 1fr / 1fr;
align-self: start;
}
.msg-container-user {
direction: rtl;
position: static;
display: inline-block;
width: 100%;
margin-top: 10px;
.msg-container > * {
grid-column: 1 / 1;
grid-row: 1 / 1;
}
.msg-container.sender {
place-items: self-start;
}
.msg-container.user {
place-items: self-end;
}
.msg-box {
background: var(--chat-bubble);
color: white;
min-width: 150px;
min-width: 100px;
border-radius: 5px;
padding: 20px 5px 5px 25px;
margin: 20px 0px 0px 25px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
padding: 18px 5px 5px 5px;
box-shadow:
0 0 2px rgba(0, 0, 0, 0.12),
0 2px 4px rgba(0, 0, 0, 0.24);
width: fit-content;
position: relative;
align-self: start;
}
.msg-box-user {
background: var(--chat-bubble);
color: white;
text-align: -webkit-left;
min-width: 150px;
border-radius: 5px;
padding: 20px 15px 10px 5px;
margin: 0px 35px 0px 25px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
width: fit-content;
.msg-box.sender {
margin: 25px 25px 0px 35px;
}
.msg-box.user {
text-align: left;
margin: 25px 35px 0px 0px;
}
.msg-box-user-temp {
@ -217,26 +230,15 @@ h1 {
.user-img {
display: inline-block;
position: relative;
border-radius: 50%;
height: 50px;
width: 50px;
z-index: 5;
align-self: start;
}
.user-img-user {
display: inline-block;
border-radius: 50%;
height: 50px;
width: 50px;
position: absolute;
z-index: 5;
}
.messages {
margin-left: 20px;
}
.messages-user {
.messages.user {
margin-right: 20px;
}
@ -250,10 +252,6 @@ h1 {
color: var(--chat-bubble-message-temp);
}
/* .msg:first-of-type {
margin-top: 8px;
} */
.timestamp {
color: var(--chat-bubble-header);
font-size: 10pt;
@ -266,27 +264,22 @@ h1 {
}
.username {
float: left;
background-color: var(--main-color4);
margin-left: 25px;
color: white;
position: relative;
padding: 5px 5px 5px 30px;
border-radius: 5px;
top: 10px;
z-index: 1;
z-index: 3;
align-self: start;
}
.username-user {
background-color: var(--main-color4);
margin-right: 25px;
color: white;
padding: 5px 40px 5px 15px;
border-radius: 5px;
margin: 0px 30px 5px 5px;
top: 15px;
position: relative;
z-index: 1;
.username.sender {
padding: 0px 5px 5px 30px;
margin: 20px 5px 5px 25px;
}
.username.user {
padding: 0px 30px 5px 5px;
margin: 20px 30px 5px 5px;
}
.username-temp {
@ -295,68 +288,28 @@ h1 {
.post-time {
font-size: 8pt;
padding: 3px 5px 0px 15px;
color: white;
display: inline-block;
background-color: var(--main-color4);
right: 15px;
top: -19px;
position: relative;
z-index: 2;
border-radius: 5px;
text-align: center;
align-self: start;
}
.post-time.sender {
padding: 5px 5px 5px 15px;
margin: 0px 0px 0px 50px;
}
.post-time-user {
font-size: 8pt;
padding: 3px 15px 0px 5px;
margin: 5px -15px 0px -10px;
color: white;
display: inline-block;
background-color: var(--main-color4);
right: 60px;
top: -19px;
position: relative;
z-index: 2;
border-radius: 5px;
text-align: center;
.post-time.user {
padding: 5px 15px 5px 5px;
margin: 0px 50px 0px 0px;
}
/* .msg-self .msg-box {
border-radius: 6px 6px 6px 6px;
background: var(--main-color1);
float: right;
}
.msg-self .user-img {
align-items: center;
}
.msg-self .msg {
text-align: justify;
text-justify: inter-word;
} */
.mmg {
display: flex;
}
.icon-container {
height: 50px;
position: absolute;
left: 0;
display: flex;
align-items: center;
}
.icon-container-user {
direction: ltr;
height: 50px;
position: absolute;
display: flex;
align-items: center;
}
.img {
height: 100%;
width: 100%;
@ -365,19 +318,18 @@ h1 {
.status-circle {
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: -15px;
z-index: 6;
margin-top: -30px;
position: relative;
align-self: start;
}
.status-circle-user {
width: 20px;
height: 20px;
border-radius: 50%;
z-index: 6;
margin-top: -30px;
.status-circle.sender {
margin-left: 40px;
}
.status-circle.user {
margin-right: 40px;
}
.menu-select {
@ -552,3 +504,77 @@ h1 {
.checkbox:checked + .toggle-small {
background-color: var(--main-color1);
}
.emotes {
position: relative;
cursor: pointer;
}
.dark {
display: none;
position: absolute;
z-index: 1;
top: -400px;
}
.emotes:hover .dark {
display: block;
}
.fi {
position: relative;
z-index: 5;
border-radius: 50%;
}
.translation-header {
background-color: var(--main-color4);
border-radius: 5px;
width: fit-content;
padding: 5px;
margin: 10px 0px 5px -5px;
position: relative;
}
.translation-message {
position: relative;
margin: 20px 0px 0px 0px;
}
.translation-message.user {
margin: -20px 0px 0px 0px;
}
.translation-icon {
position: relative;
padding: 0px 0px 0px 0px;
margin: -45px 0px 0px -40px;
top: 30px;
}
.language-icon {
position: relative;
top: 45px;
}
.flag-icon {
width: 20px !important;
height: 20px !important;
left: 18px;
}
.flag-icon.user {
left: -18px;
top: -15px;
}
.user-flag {
left: unset;
right: 18px;
top: -65px;
}
.emote {
width: 28px;
height: 28px;
}

View file

@ -1,9 +1,9 @@
input[type="radio"]:checked {
input[type='radio']:checked {
visibility: hidden;
position: absolute;
}
input[type="radio"] {
input[type='radio'] {
visibility: hidden;
position: absolute;
}
@ -12,30 +12,30 @@ label.btn span {
font-size: 1.5em;
}
label input[type="radio"]~i.fa.fa-square {
label input[type='radio'] ~ i.fa.fa-square {
color: var(--main-color3);
display: inline;
}
label input[type="radio"]~i.fa.fa-check-square {
label input[type='radio'] ~ i.fa.fa-check-square {
display: none;
}
label input[type="radio"]:checked~i.fa.fa-square {
label input[type='radio']:checked ~ i.fa.fa-square {
display: none;
}
label input[type="radio"]:checked~i.fa.fa-check-square {
label input[type='radio']:checked ~ i.fa.fa-check-square {
display: inline;
color: var(--main-color2);
}
label:hover input[type="radio"]~i.fa {
label:hover input[type='radio'] ~ i.fa {
color: var(--main-color1);
/* filter: brightness(150%); */
}
div[data-toggle="buttons"] label {
div[data-toggle='buttons'] label {
display: inline-block;
padding: 3px 12px;
margin-bottom: 0;
@ -55,8 +55,8 @@ div[data-toggle="buttons"] label {
user-select: none;
}
div[data-toggle="buttons"] label:active,
div[data-toggle="buttons"] label.active {
div[data-toggle='buttons'] label:active,
div[data-toggle='buttons'] label.active {
-webkit-box-shadow: none;
box-shadow: none;
}

View file

@ -36,25 +36,14 @@ html {
box-sizing: inherit;
}
html,
body {
height: 100%;
margin: 0;
/* border-top-left-radius: 20px; */
/* border-top-right-radius: 20px; */
overflow-x: hidden;
}
body {
font-family: 'Segoe UI', sans-serif;
background: transparent;
}
/* Styling of window frame and titlebar */
body {
/* border: 1px solid #48545c; */
overflow-y: hidden;
position: relative;
/* overflow-y: hidden;
overflow-x: hidden; */
}
#titlebar {
@ -219,8 +208,18 @@ body {
display: inline-block;
background-color: transparent;
cursor: pointer;
font-family: 'NotoColorEmojiLimited', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-family:
'NotoColorEmojiLimited',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol';
left: 50%;
transform: translateX(-50%);
}
@ -232,8 +231,18 @@ body {
width: 55px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 2;
font-family: 'NotoColorEmojiLimited', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-family:
'NotoColorEmojiLimited',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol';
}
.language-item {
@ -242,12 +251,24 @@ body {
background-color: var(--top-bar);
}
.language-item:hover {
/* filter: brightness(150%); */
}
/* .language-item:hover {
filter: brightness(150%);
} */
@font-face {
font-family: NotoColorEmojiLimited;
unicode-range: U+1F1E6-1F1FF;
src: url(https://raw.githack.com/googlefonts/noto-emoji/main/fonts/NotoColorEmoji.ttf);
}
#drop-zone {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
text-align: center;
padding: 20px;
margin: 20px auto;
}
#dropped-file {
font-weight: bold;
}

View file

@ -61,11 +61,6 @@
color: var(--main-color2);
}
.hdp:hover {
position: fixed;
/* filter: brightness(150%); */
}
.menu .items .item-active {
background: -webkit-linear-gradient(left, var(--main-color2) 10%, var(--main-color2), var(--main-color1) 10%, var(--main-color1) 10%);
color: var(--main-color2);
@ -85,7 +80,9 @@
line-height: 1.5em;
font-family: Helvetica;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
box-shadow:
rgba(0, 0, 0, 0.16) 0px 3px 6px,
rgba(0, 0, 0, 0.23) 0px 3px 6px;
transition: 0.3s ease-in-out;
}
@ -96,7 +93,9 @@
line-height: 1.5em;
font-family: Helvetica;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
box-shadow:
rgba(0, 0, 0, 0.16) 0px 3px 6px,
rgba(0, 0, 0, 0.23) 0px 3px 6px;
transition: 0.3s ease-in-out;
}
@ -132,7 +131,7 @@
left: 50px;
cursor: pointer;
display: flex;
z-index: 1;
z-index: 2;
transition: 0.3s ease-in-out;
}
@ -224,3 +223,81 @@
background-color: var(--main-color4-temp);
height: 100%;
}
.pop {
position: relative;
cursor: pointer;
}
.pop-selection {
width: 20px !important;
height: 20px !important;
}
.miniText {
position: absolute;
font-size: 8pt;
color: white;
padding: 3px;
}
.pop-content {
display: none;
position: absolute;
background-color: var(--main-color3);
z-index: 1;
height: 400px;
width: max-content;
overflow: auto;
grid-template-columns: repeat(3, 1fr);
top: -400px;
color: white;
border: 1px solid #444;
}
pop-content div {
color: white;
text-decoration: none;
display: block;
}
.pop-content div:hover {
backdrop-filter: invert(50%);
}
.pop:hover .pop-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.network-select {
position: relative;
cursor: pointer;
}
.send-to-channel {
display: none;
position: absolute;
background-color: var(--main-color3);
top: calc(-50% - 3px);
border: 1px solid #444;
z-index: 1;
width: 107px;
}
send-to-channel div {
color: white;
text-decoration: none;
display: block;
}
send-to-channel div:hover {
backdrop-filter: invert(50%);
}
.network-select:hover .send-to-channel {
display: block;
backdrop-filter: invert(50%);
grid-template-columns: repeat(3, 1fr);
}
.language-select {
position: relative;
}

View file

@ -53,19 +53,27 @@ input[type='range'].styled-slider::-webkit-slider-runnable-track {
}
input[type='range'].styled-slider.slider-progress1::-webkit-slider-runnable-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #1a1a1a;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#1a1a1a;
}
input[type='range'].styled-slider.slider-progress2::-webkit-slider-runnable-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #1a1a1a;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#1a1a1a;
}
input[type='range'].styled-slider.slider-progress3::-webkit-slider-runnable-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #1a1a1a;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#1a1a1a;
}
input[type='range'].styled-slider.slider-progress4::-webkit-slider-runnable-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #1a1a1a;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#1a1a1a;
}
/*mozilla*/
@ -87,19 +95,27 @@ input[type='range'].styled-slider::-moz-range-track {
}
input[type='range'].styled-slider.slider-progress1::-moz-range-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #464646;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#464646;
}
input[type='range'].styled-slider.slider-progress2::-moz-range-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #464646;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#464646;
}
input[type='range'].styled-slider.slider-progress3::-moz-range-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #464646;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#464646;
}
input[type='range'].styled-slider.slider-progress4::-moz-range-track {
background: linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat, #464646;
background:
linear-gradient(var(--main-color1), var(--main-color1)) 0 / var(--sx) 100% no-repeat,
#464646;
}
/*ms*/

View file

@ -152,8 +152,8 @@ input:checked + label {
bottom: 0px;
}
.tabx-bar .tabx::after {
}
/* .tabx-bar .tabx::after {
} */
.tabx-bar .tabx:hover {
padding-bottom: 10px;
@ -325,7 +325,9 @@ input[type='lol'] {
margin: 0.5rem;
opacity: 0;
transform: translateY(100%);
animation: toastAnimation 0.5s ease-in-out forwards, toastDisappear 0.5s ease-in-out 9s forwards;
animation:
toastAnimation 0.5s ease-in-out forwards,
toastDisappear 0.5s ease-in-out 9s forwards;
}
/* Apply different colors based on the toast type */
@ -375,43 +377,19 @@ input[type='lol'] {
.tooltip {
position: absolute;
display: inline-block;
visibility: hidden;
font-size: 12px;
line-height: 20px;
font-size: 12pt;
padding: 5px;
background: var(--main-color3);
border-radius: 5px;
visibility: hidden;
opacity: 1;
box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
transition: opacity 0.3s, visibility 0s;
color: var(--main-color2);
font-family: 'xxii_avenmedium';
z-index: 999;
max-width: 200px;
width: max-content;
}
/* .tooltip .tooltiptext {
width: 120px;
background-color: black;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 0;
}
.tooltip .tooltiptext::after {
content: "";
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: black transparent transparent transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
} */
div[type='text']:disabled {
background: #4b4b4b;
display: none;

View file

@ -0,0 +1,88 @@
.token-autocomplete-container {
display: inline-flex;
flex-wrap: wrap;
width: 300px;
}
.token-autocomplete-container,
.token-autocomplete-container * {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
.token-autocomplete-container .token-autocomplete-input {
color: var(--main-color2);
font-size: 16px;
line-height: 32px;
margin: 4px 2px;
padding: 0px 8px;
height: 40px;
width: 300px;
background-color: var(--main-color3);
border-radius: 20px;
}
.token-autocomplete-container .token-autocomplete-input:empty::before {
content: attr(data-placeholder);
color: gray;
}
.token-autocomplete-container .token-autocomplete-token {
font-size: 16px;
line-height: 32px;
background-color: var(--main-color3);
margin: 4px 2px;
border-radius: 32px;
padding: 0px 8px;
pointer-events: none;
}
.token-autocomplete-container .token-autocomplete-token:hover {
background-color: #ef9a9a;
}
.token-autocomplete-container .token-autocomplete-token .token-autocomplete-token-delete {
cursor: pointer;
font-size: 24px;
line-height: 16px;
margin-left: 4px;
pointer-events: auto;
border-radius: 50%;
height: 24px;
width: 24px;
display: inline-block;
text-align: center;
}
.token-autocomplete-container .token-autocomplete-token .token-autocomplete-token-delete:hover {
background-color: #e55858;
}
.token-autocomplete-container .token-autocomplete-suggestions {
display: none;
width: 100%;
list-style-type: none;
padding: 0px;
margin: 0px;
}
.token-autocomplete-container .token-autocomplete-suggestions li {
width: 100%;
padding: 8px;
cursor: pointer;
}
.token-autocomplete-container .token-autocomplete-suggestions li.token-autocomplete-suggestion-active {
color: #747474;
background-color: #fdfdfd;
}
.token-autocomplete-container .token-autocomplete-suggestions li.token-autocomplete-suggestion-highlighted {
background-color: #95caec;
}
.token-autocomplete-container .token-autocomplete-suggestions li .token-autocomplete-suggestion-description {
display: block;
font-size: 0.7em;
color: #808080;
}

View file

@ -74,22 +74,22 @@ textarea {
color: var(--main-color2);
width: 50px;
cursor: pointer;
text-shadow: 0 0 5px #070607, 0 0 5px #070607, 0 0 5px #070607;
/* transition: all 0.15s ease-in-out; */
text-shadow:
0 0 5px #070607,
0 0 5px #070607,
0 0 5px #070607;
transition: all 0.15s ease-in-out;
text-align: center;
}
.SmallButton:hover {
/* color: var(--main-color1); */
width: 50px;
color: var(--main-color1);
cursor: pointer;
/* filter: brightness(150%); */
}
.SmallButton:active {
color: var(--main-color1);
transform: translateY(4px);
text-shadow: 0 0 5px #000, 0 0 5px #000, 0 0 5px #000;
}
.AdvancedMenuButton {
@ -103,7 +103,9 @@ textarea {
font-family: 'xxii_avenmedium';
font-size: 14pt;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: box-shadow 0.3s ease, background-color 0.3s ease;
transition:
box-shadow 0.3s ease,
background-color 0.3s ease;
/* Add a smooth transition for box-shadow and background-color */
}

BIN
src/images/dlive-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
src/images/dlive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/images/trovo-icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/images/trovo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/images/youtube-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
src/images/youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -17,6 +17,8 @@
<link rel="stylesheet" href="./css/tts-menu.css" />
<link rel="stylesheet" href="./css/volume-slider.css" />
<link rel="stylesheet" href="./css/sliders.css" />
<link rel="stylesheet" href="./css/token-autocomplete.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
@ -25,6 +27,8 @@
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./css/logger.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
<!--#endregion -->
</head>
@ -42,13 +46,13 @@
<select name="defaultLanguage" class="top-select" id="language"></select>
</div>
<div id="window-controls">
<div class="button" id="min-button" tip="Minimize window" tip-left>
<div class="button" id="min-button" tip="Minimize window">
<i class="fa-solid fa-window-minimize"></i>
</div>
<div class="button" id="max-button" tip="Maximize window" tip-left>
<div class="button" id="max-button" tip="Maximize window">
<i class="fa-solid fa-window-maximize"></i>
</div>
<div class="button" id="close-button" tip="Close application" tip-left>
<div class="button" id="close-button" tip="Close application">
<i class="fa-solid fa-xmark"></i>
</div>
</div>
@ -74,15 +78,21 @@
<li class="item" tip="Chat Creator" id="btnChatCreator">
<i class="fa-solid fa-spray-can"></i>
</li>
<li class="item" tip="BrowserSource ChatBubble" id="btnBrowsersourceChat">
<li class="item" tip="BrowserSource ChatBubble" id="btnBrowsersourceChatBubble">
<i class="fa fa-history menu-icon"></i>
</li>
<li class="item" tip="BrowserSource Vtuber" id="btnBrowsersourceVtuber">
<i class="fa fa-user menu-icon"></i>
<i class="fa-solid fa-address-card menu-icon"></i>
</li>
<li class="item" tip="BrowserSource PNGTuber" id="btnBrowsersourcePNGTuber">
<i class="fa-solid fa-image-portrait menu-icon"></i>
</li>
<li class="item" tip="Logs" id="btnLogs">
<i class="fa fa-clipboard menu-icon"></i>
</li>
<li class="item" tip="FaceMask" id="btnFaceMask">
<i class="fa-solid fa-masks-theater"></i>
</li>
<li class="item active-mic" tip="Info" id="btnConfiguration">
<i class="fa-solid fa-microphone-lines"></i>
</li>
@ -114,14 +124,42 @@
</div>
<!--#endregion -->
<!-- #region Commands Screen-->
<div class="OptionPanel" id="BrowsersourceChat"></div>
<!-- #region Chat-->
<div class="OptionPanel" id="BrowsersourceChatWindow"></div>
<!--#endregion -->
<!-- #region Commands Screen-->
<!-- #region ChatBubble-->
<div class="OptionPanel" id="BrowsersourceChatBubble"></div>
<!--#endregion -->
<!-- #region Vtuber-->
<div class="OptionPanel" id="BrowsersourceVtuber"></div>
<!--#endregion -->
<!-- #region Vtuber-->
<div class="OptionPanel" id="BrowsersourcePNGTuber"></div>
<!--#endregion -->
<!-- #region FaceMask-->
<div class="OptionPanel" id="FaceMask">
<!-- <div class="select">
<label for="audioSource">Audio source: </label
><select id="audioSource"></select>
</div>
<div class="select">
<label for="videoSource">Video source: </label
><select id="videoSource"></select>
</div> -->
<!-- <video id="video" autoplay></video> -->
<div id="drop-zone">
<p>Drag a file here</p>
<p id="dropped-file"></p>
</div>
</div>
<!--#endregion -->
<!-- #region Configuration Screen-->
<div class="OptionPanel" id="Configuration">
<div id="tstx">
@ -132,9 +170,9 @@
<img class="AdvancedMenuIcon" src="./images/settings.png" alt=" " />
<div class="AdvancedMenuLabel3">General settings</div>
</legend>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuRow">
<div class="AdvancedMenuLabel">Port</div>
<input type="text" class="fname inputServer" id="PORT" />
<input type="text" class="fname" id="PORT" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_PORT"
@ -142,14 +180,41 @@
></i>
</div>
<div class="AdvancedMenuRow">
<div class="AdvancedMenuLabel">Zoom level %</div>
<input type="text" class="fname" id="ZOOMLEVEL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_ZOOMLEVEL"
tip="Port to use to host additional services"
></i>
</div>
<div class="AdvancedMenuRow">
<div class="AdvancedMenuLabel">Open Settings file</div>
<button type="text" class="AdvancedMenuButton" id="OPEN_SETTINGS_FILE"><i class="fa-solid fa-file-lines"></i></button>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_OPEN_SETTINGS_FILE"
tip="Open the settings file"
></i>
</div>
</fieldset>
<fieldset id="TTSMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable TTS">
<img class="AdvancedMenuIcon" src="./images/tts.png" alt=" " />
<input type="checkbox" id="USE_TTS" class="checkbox" />
<label for="USE_TTS" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable TTS</div>
</legend>
<div class="AdvancedMenuRow inputTTS" style="height: 0; visibility: hidden">
<div class="AdvancedMenuLabel">Default TTS Service</div>
<select class="menu-select" name="primaryTTSService" id="primaryTTSService"></select>
</div>
<div class="AdvancedMenuRow">
<div class="AdvancedMenuRow languageDetectionInput" style="height: 0; visibility: hidden">
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS Service</div>
<select class="menu-select" name="secondaryTTSService" id="secondaryTTSService"></select>
</div>
<div class="AdvancedMenuRow">
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">TTS Output Device</div>
<select class="menu-select" name="ttsAudioDevice" id="ttsAudioDevice"></select>
<i
@ -158,7 +223,7 @@
tip="Outputting to specific device will NOT work with voicemeter"
></i>
</div>
<div class="AdvancedMenuRow">
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">TTS Volume</div>
<div class="slider-container">
<input id="ttsVolumeSlider" class="styled-slider slider-progress1" type="range" />
@ -168,79 +233,6 @@
<input type="text" id="ttsVolume" class="inputBox" />
</div>
</div>
</fieldset>
<fieldset id="STTMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable STT">
<img class="AdvancedMenuIcon" src="./images/stt.png" alt=" " />
<input type="checkbox" id="USE_STT" class="checkbox" />
<label for="USE_STT" class="toggle-small "></label>
<div class="AdvancedMenuLabel3">Enable STT</div>
</legend>
<div class="AdvancedMenuRow inputSTT">
<div class="AdvancedMenuLabel">STT Input Device</div>
<select class="menu-select" name="microphone" id="microphone"></select>
</div>
<div class="AdvancedMenuRow voiceLanguageDetection inputSTT">
<div class="AdvancedMenuLabel">Voice Language</div>
<select class="menu-select" name="sttModel" id="sttModel" tip="Language Service to use"></select>
</div>
</fieldset>
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable Language service">
<img class="AdvancedMenuIcon" src="./images/translate.png" alt />
<input type="checkbox" id="USE_DETECTION" class="checkbox" />
<label for="USE_DETECTION" class="toggle-small "></label>
<div class="AdvancedMenuLabel3">Enable Language detection</div>
</legend>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Language detection service</div>
<select
class="menu-select"
name="language"
id="languageService"
tip="Language Service to use"
></select>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Translate incoming chat messages to</div>
<label for="USE_CHAT_LANGUAGE_DETECTION" class="toggle-small "></label>
<input type="checkbox" id="USE_CHAT_LANGUAGE_DETECTION" class="checkbox" />
<select
class="menu-select"
name="language"
id="translateChatMessageLanguage"
tip="Language Service to use"
></select>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Default TTS language</div>
<select
class="menu-select"
name="defaultLanguage"
id="defaultLanguage"
tip="Language Service to use"
></select>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS language</div>
<select
class="menu-select"
name="secondaryLanguage"
id="secondaryLanguage"
tip="Language Service to use"
></select>
</div>
</fieldset>
<fieldset id="TTSMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable TTS">
<img class="AdvancedMenuIcon" src="./images/tts.png" alt=" " />
<input type="checkbox" id="USE_TTS" class="checkbox" />
<label for="USE_TTS" class="toggle-small "></label>
<div class="AdvancedMenuLabel3">Enable internal TTS</div>
</legend>
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">Default Internal Voice</div>
<select class="menu-select" name="primaryVoice" id="primaryVoice"></select>
@ -249,36 +241,123 @@
<div class="AdvancedMenuLabel">Test default Internal Voice</div>
<textarea id="testPrimaryTTS">Hi, This is a test</textarea>
<div class="option-icon-container" tip="Test internal TTS">
<i
class="fa fa-play-circle fa-2x SmallButton option-icon-container"
id="TestDefaultTTSButton"
></i>
<i class="fa fa-play-circle fa-2x SmallButton option-icon-container" id="TestDefaultTTSButton"></i>
<label class="testLabel Basiclabel"></label>
</div>
</div>
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">2<sup>nd</sup> Internal Voice</div>
<select class="menu-select" name="secondaryVoice" id="secondaryVoice"></select>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_SECONDARY_TTS"
tip="This will only work if Language detection is enabled and a language for this voice has been selected"
></i>
</div>
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">Test 2<sup>nd</sup> Internal Voice</div>
<textarea id="testSecondaryTTS">Hi, This is a test</textarea>
<div class="option-icon-container" tip="Test internal TTS">
<i
class="fa fa-play-circle fa-2x SmallButton option-icon-container"
id="TestSecondaryTTSButton"
></i>
<i class="fa fa-play-circle fa-2x SmallButton option-icon-container" id="TestSecondaryTTSButton"></i>
<label class="testLabel Basiclabel"></label>
</div>
</div>
</fieldset>
<fieldset id="STTMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable STT">
<img class="AdvancedMenuIcon" src="./images/stt.png" alt=" " />
<input type="checkbox" id="USE_STT" class="checkbox" />
<label for="USE_STT" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable STT</div>
</legend>
<div class="AdvancedMenuRow inputSTT">
<div class="AdvancedMenuLabel">STT Input Device</div>
<select class="menu-select" name="microphone" id="microphone"></select>
</div>
<div class="AdvancedMenuRow voiceLanguageDetection inputSTT">
<div class="AdvancedMenuLabel">Voice Language model</div>
<select class="menu-select" name="sttModel" id="sttModel" tip="Language Service to use"></select>
<i
class="fa-solid fa-folder-open fa-2x SmallButton option-icon-container"
id="Info_VOICE_MODELS_FOLDER"
tip="Open Voice models folder"
></i>
</div>
</fieldset>
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable Language service">
<img class="AdvancedMenuIcon" src="./images/translate.png" alt />
<input type="checkbox" id="USE_DETECTION" class="checkbox" />
<label for="USE_DETECTION" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Language detection</div>
</legend>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Default TTS service language</div>
<select class="menu-select" name="defaultLanguage" id="defaultLanguage" tip="Language Service to use"></select>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_PRIMARY_TTS_LANGUAGE"
tip="When the selected language is detected Your Primary TTS voice will sound"
></i>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">2<sup>nd</sup> TTS service language</div>
<select class="menu-select" name="secondaryLanguage" id="secondaryLanguage" tip="Language Service to use"></select>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_SECONDARY_TTS_LANGUAGE"
tip="When the selected language is detected Your Secondary TTS voice will sound"
></i>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Translate chat messages to</div>
<select class="menu-select" name="language" id="TRANSLATE_TO" tip="Language Service to use"></select>
</div>
<div class="AdvancedMenuRow languageDetectionInput TRANSLATE_TO">
<div class="AdvancedMenuLabel">Broadcast translation to chat</div>
<input type="checkbox" id="BROADCAST_TRANSLATION" class="checkbox TRANSLATE_TO" />
<label for="BROADCAST_TRANSLATION" class="toggle-small" style="margin-right: 260px"></label>
</div>
<div class="AdvancedMenuRow languageDetectionInput TRANSLATE_TO">
<div class="AdvancedMenuLabel">Output translation to TTS</div>
<input type="checkbox" id="OUTPUT_TO_TTS" class="checkbox TRANSLATE_TO" />
<label for="OUTPUT_TO_TTS" class="toggle-small" style="margin-right: 260px"></label>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_OUTPUT_TO_TTS"
tip="All translated messages will be send to primary TTS voice but if message is detected in Secondary TTS language it will output it to the Secondary TTS voice"
></i>
</div>
<div class="AdvancedMenuRow languageDetectionInput">
<div class="AdvancedMenuLabel">Send translated messages</div>
<input type="checkbox" id="SEND_TRANSLATION" class="checkbox" />
<label for="SEND_TRANSLATION" class="toggle-small" style="margin-right: 260px"></label>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_SEND_TRANSLATION"
tip="Enable sending translated messages to the chat."
></i>
</div>
</fieldset>
<fieldset id="NotificationMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable notification sounds">
<img class="AdvancedMenuIcon" src="./images/sound.png" alt=" " />
<input type="checkbox" id="USE_NOTIFICATION_SOUNDS" class="checkbox" />
<label for="USE_NOTIFICATION_SOUNDS" class="toggle-small "></label>
<label for="USE_NOTIFICATION_SOUNDS" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable notification sounds</div>
</legend>
<div class="AdvancedMenuRow inputNotificationSound">
<div class="AdvancedMenuLabel">Notification sounds Output Device</div>
<select class="menu-select" name="notificationSoundAudioDevice" id="notificationSoundAudioDevice"></select>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_OUTPUT_NOTIFIACTION_SOUNDS"
tip="Outputting to specific device will NOT work with voicemeter"
></i>
</div>
<div class="AdvancedMenuRow inputNotificationSound">
<div class="AdvancedMenuLabel">Notification Volume</div>
<div class="slider-container">
@ -293,11 +372,7 @@
<div class="AdvancedMenuLabel">Notification Sound</div>
<select class="menu-select" name="notification" id="notification"></select>
<div class="option-icon-container">
<i
class="fa fa-play-circle fa-2x SmallButton option-icon-container"
id="SoundTestButton"
tip="Test Sound"
></i>
<i class="fa fa-play-circle fa-2x SmallButton option-icon-container" id="SoundTestButton" tip="Test Sound"></i>
<label class="testLabel Basiclabel"></label>
</div>
</div>
@ -307,18 +382,9 @@
<legend class="legendStyle" tip="Enable/Disable Twitch">
<img class="AdvancedMenuIcon" src="./images/twitch.png" alt=" " />
<input type="checkbox" id="USE_TWITCH" class="checkbox" />
<label for="USE_TWITCH" class="toggle-small "></label>
<label for="USE_TWITCH" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Twitch</div>
</legend>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Channel Name</div>
<input type="text" class="fname inputTwitch" id="TWITCH_CHANNEL_NAME" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_TWITCH_CHANNEL_NAME"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Oauth Token</div>
<input
@ -326,15 +392,20 @@
type2="text"
class="fname inputTwitch"
id="TWITCH_OAUTH_TOKEN"
placeholder="click the ? icon to get the OAuth token"
placeholder="click the key icon to get the OAuth token"
/>
<button class="password-toggle-btn password-toggle-btn1">
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
</button>
<i class="fa-solid fa-key fa-2x SmallButton option-icon-container" id="Info_TWITCH_USERNAME" tip="Get OAuth Token"></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Channel Name</div>
<input type="text" class="fname inputTwitch" id="TWITCH_CHANNEL_NAME" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_USERNAME"
tip="Get OAuth Token"
id="Info_TWITCH_CHANNEL_NAME"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
@ -346,19 +417,191 @@
tip="Test Twitch credentials"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">BetterTTV Channels</div>
<div id="sample"></div>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_BETTERTTV_CHANNELS"
tip="Insert the channel names of the betterTTV emotes you want"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Get BetterTTV emotes</div>
<button type="text" class="AdvancedMenuButton" id="GetBetterTtvEmotes">
<img src="https://cdn.betterttv.net/emote/5f1b0186cf6d2144653d2970/3x.webp" width="30px" height="30px" />
</button>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_BETTERTTV_EMOTES"
tip="Test Twitch credentials"
></i>
</div>
</fieldset>
<fieldset id="YoutubeMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable Youtube">
<img class="AdvancedMenuIcon" src="./images/youtube.png" alt=" " />
<input type="checkbox" id="USE_YOUTUBE" class="checkbox" />
<label for="USE_YOUTUBE" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Youtube</div>
</legend>
<!-- <div class="AdvancedMenuRow inputYoutube">
<div class="AdvancedMenuLabel">API Key</div>
<input
type="password"
type2="text"
class="fname inputYoutube"
id="YOUTUBE_OAUTH_TOKEN"
placeholder="click the key icon to get the OAuth token"
/>
<button class="password-toggle-btn password-toggle-btn2">
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
</button>
<i class="fa-solid fa-key fa-2x SmallButton option-icon-container" id="Info_YOUTUBE_USERNAME" tip="Get OAuth Token"></i>
</div> -->
<div class="AdvancedMenuRow inputYoutube">
<div class="AdvancedMenuLabel">Channel Handle</div>
<input type="text" class="fname inputYoutube" id="YOUTUBE_CHANNEL_HANDLE" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_YOUTUBE_CHANNEL_HANDLE"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputYoutube">
<div class="AdvancedMenuLabel">Channel ID</div>
<input type="text" class="fname inputYoutube" id="YOUTUBE_CHANNEL_ID" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_YOUTUBE_CHANNEL_ID"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputYoutube">
<div class="AdvancedMenuLabel">Live ID</div>
<input type="text" class="fname inputYoutube" id="YOUTUBE_LIVE_ID" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_YOUTUBE_LIVE_ID"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputYoutube">
<div class="AdvancedMenuLabel">Test credentials</div>
<button type="text" class="AdvancedMenuButton" id="TestYoutubeCredentials">Test</button>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_YOUTUBE_TEST_CREDENTIALS"
tip="Test Youtube credentials"
></i>
</div>
</fieldset>
<fieldset id="TrovoMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable Trovo">
<img class="AdvancedMenuIcon" src="./images/trovo.png" alt=" " />
<input type="checkbox" id="USE_TROVO" class="checkbox" />
<label for="USE_TROVO" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Trovo</div>
</legend>
<div class="AdvancedMenuRow inputTrovo">
<div class="AdvancedMenuLabel">Oauth Token</div>
<input
type="password"
type2="text"
class="fname inputTrovo"
id="TROVO_OAUTH_TOKEN"
placeholder="click the key icon to get the OAuth token"
/>
<button class="password-toggle-btn password-toggle-btn2">
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
</button>
<i class="fa-solid fa-key fa-2x SmallButton option-icon-container" id="Info_TROVO_USERNAME" tip="Get OAuth Token"></i>
</div>
<div class="AdvancedMenuRow inputTrovo">
<div class="AdvancedMenuLabel">Channel Name</div>
<input type="text" class="fname inputTrovo" id="TROVO_CHANNEL_NAME" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_TROVO_CHANNEL_NAME"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputTrovo">
<div class="AdvancedMenuLabel">Test credentials</div>
<button type="text" class="AdvancedMenuButton" id="TestTrovoCredentials">Test</button>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_TROVO_TEST_CREDENTIALS"
tip="Test Trovo credentials"
></i>
</div>
</fieldset>
<fieldset id="DLiveMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable DLive">
<img class="AdvancedMenuIcon" src="./images/dlive.png" alt=" " />
<input type="checkbox" id="USE_DLIVE" class="checkbox" />
<label for="USE_DLIVE" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable DLive</div>
</legend>
<div class="AdvancedMenuRow inputDLive">
<div class="AdvancedMenuLabel">API Key</div>
<input
type="password"
type2="text"
class="fname inputDLive"
id="DLIVE_OAUTH_TOKEN"
placeholder="click the key icon to get the OAuth token"
/>
<button class="password-toggle-btn password-toggle-btn2">
<span class="password-toggle-icon"><i class="fa-regular fa-eye-slash"></i></span>
</button>
<i class="fa-solid fa-key fa-2x SmallButton option-icon-container" id="Info_DLIVE_USERNAME" tip="Get OAuth Token"></i>
</div>
<div class="AdvancedMenuRow inputDLive">
<div class="AdvancedMenuLabel">Channel Name</div>
<input type="text" class="fname inputDLive" id="DLIVE_CHANNEL_NAME" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_DLIVE_CHANNEL_NAME"
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputDLive">
<div class="AdvancedMenuLabel">Test credentials</div>
<button type="text" class="AdvancedMenuButton" id="TestDLiveCredentials">Test</button>
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_DLIVE_TEST_CREDENTIALS"
tip="Test DLive credentials"
></i>
</div>
</fieldset>
<fieldset id="AdvancedMenuServer" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable Server">
<img class="AdvancedMenuIcon" src="./images/server.png" alt />
<input type="checkbox" id="USE_MODULES" class="checkbox" />
<label for="USE_MODULES" class="toggle-small "></label>
<label for="USE_MODULES" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Modules</div>
</legend>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuLabel">Use Vtuber</div>
<div class="AdvancedMenuLabel">PNGtuber</div>
<input type="checkbox" id="USE_PNGTUBER" class="checkbox" />
<label for="USE_PNGTUBER" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="PNGTUBER_URL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_PNGTUBER"
tip="You can use it as a browsersource on http://localhost:PORT/pngtuber"
></i>
</div>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuLabel">Vtuber</div>
<input type="checkbox" id="USE_VTUBER" class="checkbox" />
<label for="USE_VTUBER" class="toggle-small "></label>
<label for="USE_VTUBER" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="VTUBER_URL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
@ -367,23 +610,52 @@
></i>
</div>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuLabel">Use ChatBubble</div>
<div class="AdvancedMenuLabel">external Chat window</div>
<input type="checkbox" id="USE_CHATWINDOW" class="checkbox" />
<label for="USE_CHATWINDOW" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="CHATWINDOW_URL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_CHATWINDOW"
tip="You can use it as a browsersource on http://localhost:PORT/chat"
></i>
</div>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuLabel">ChatBubble</div>
<input type="checkbox" id="USE_CHATBUBBLE" class="checkbox" />
<label for="USE_CHATBUBBLE" class="toggle-small "></label>
<label for="USE_CHATBUBBLE" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="CHATBUBBLE_URL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_CHATBUBBLE"
tip="You can use it as a browsersource on http://localhost:PORT/chatbubble"
></i>
</div>
<div class="AdvancedMenuRow inputServer">
<div class="AdvancedMenuLabel">FaceMask</div>
<input type="checkbox" id="USE_FACEMASK" class="checkbox" />
<label for="USE_FACEMASK" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="FACEMASK_URL" />
<i class="fa fa-question-circle fa-2x SmallButton option-icon-container" id="Info_FACEMASK" tip="WIP"></i>
</div>
<!-- <div class="AdvancedMenuRow inputServer" style="height: 0; visibility: hidden">
<div class="AdvancedMenuLabel">Use Finger</div>
<input type="checkbox" id="USE_CHATBUBBLE" class="checkbox" />
<label for="USE_CHATBUBBLE" class="toggle-small"></label>
<input type="url" type2="text" class="fname inputServer" id="CHATBUBBLE_URL" />
<i
class="fa fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_CHATBUBBLE"
tip="You can use it as a browsersource on http://localhost:PORT/chat"
></i>
</div>
</div> -->
</fieldset>
<fieldset id="AdvancedMenuAmazon" class="AdvancedMenu">
<fieldset id="AdvancedMenuAmazon" class="AdvancedMenu" style="height: 0; visibility: hidden">
<legend class="legendStyle" tip="Enable/Disable Amazon">
<img class="AdvancedMenuIcon" src="./images/amazon.png" alt />
<input type="checkbox" id="USE_AMAZON" class="checkbox" />
<label for="USE_AMAZON" class="toggle-small "></label>
<label for="USE_AMAZON" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Amazon services</div>
</legend>
<div class="AdvancedMenuRow inputAmazon">
@ -456,11 +728,11 @@
</div>
</fieldset>
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu">
<fieldset id="AdvancedMenuGoogle" class="AdvancedMenu" style="height: 0; visibility: hidden">
<legend class="legendStyle" tip="Enable/Disable Google service">
<img class="AdvancedMenuIcon" src="./images/google.png" alt />
<input type="checkbox" id="USE_GOOGLE" class="checkbox" />
<label for="USE_GOOGLE" class="toggle-small "></label>
<label for="USE_GOOGLE" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Google services</div>
</legend>
<div class="AdvancedMenuRow inputGoogle">
@ -530,10 +802,36 @@
<div id="chatBox" class="message-window"></div>
<!-- User input box-->
<div id="userInput" class="chat-input">
<div class="emotes">
<button class="SmallButton">
<i class="fa-regular fa-grin fa-2x" id="emojis" aria-hidden="true"></i>
</button>
<emoji-picker class="dark"></emoji-picker>
</div>
<div class="pop in send-translation">
<div class="miniText">In</div>
<button class="SmallButton">
<i class="fa-solid fa-globe fa-2x" aria-hidden="true"></i>
</button>
<div class="pop-content" id="SEND_TRANSLATION_IN"></div>
</div>
<div class="pop out send-translation">
<div class="miniText">Out</div>
<button class="SmallButton">
<i class="fa-solid fa-globe fa-2x" aria-hidden="true"></i>
</button>
<div class="pop-content" id="SEND_TRANSLATION_OUT"></div>
</div>
<!-- User text input-->
<input id="textInput" class="input-box" type="text" name="msg" placeholder="Tap 'Enter' to send a message" />
<!-- Send button-->
<div class="network-select">
<button class="SmallButton">
<i class="fa-brands fa-stack-exchange fa-2x" aria-hidden="true"></i>
</button>
<div class="send-to-channel" id="SEND_TO_CHANNELS"></div>
</div>
<button class="SmallButton" id="SendButton">
<i class="fa fa-play-circle fa-2x" aria-hidden="true"></i>
</button>
@ -638,7 +936,7 @@
</section>
</div>
</div>
<div id="VIEWERS_PANEL" tip="Hide Viewers" tip-left>
<div id="VIEWERS_PANEL" tip="Hide Viewers">
<div class="circle-right">
<i class="fa fa-eye hide" aria-hidden="true"></i>
</div>

View file

@ -1,3 +1,5 @@
/* global settings, callback, addVoiceService, amazonVoices, */
const https = require('https');
const querystring = require('querystring');
const aws4 = require('aws4');
@ -10,12 +12,12 @@ function getAmazonVoices() {
addVoiceService('Amazon');
let primaryVoice = document.querySelector('#primaryAmazonVoice');
let secondaryVoice = document.querySelector('#secondaryAmazonVoice');
const primaryVoice = document.querySelector('#primaryAmazonVoice');
const secondaryVoice = document.querySelector('#secondaryAmazonVoice');
function setVoicesinSelect(voiceSelect) {
const voices = Object.values(amazonVoices);
voices.forEach((voice) => {
voices.forEach(voice => {
const option = document.createElement('option');
option.classList.add('option');
@ -36,8 +38,6 @@ if (settings.AMAZON.USE_AMAZON) {
}
class PollyTTS {
constructor() {}
textToSpeech(options, callback) {
if (!options) {
return callback(new Error('Options are missing'));
@ -49,27 +49,27 @@ class PollyTTS {
VoiceId: options.voiceId || 'Mia',
SampleRate: options.sampleRate || 22050,
OutputFormat: options.outputFormat || 'mp3',
Engine: options.engine || 'neural',
Engine: options.engine || 'neural'
};
const opts = {
service: 'polly',
region: options.region || 'us-east-1',
path: `/v1/speech?${querystring.stringify(qs)}`,
signQuery: true,
signQuery: true
};
// you can also pass AWS credentials in explicitly (otherwise taken from process.env)
aws4.sign(opts, this.credentials);
https
.get(opts, (res) => {
.get(opts, res => {
if (res.statusCode !== 200) {
return callback(new Error(`Request Failed. Status Code: ${res.statusCode}`));
}
callback(null, res);
return true;
})
.on('error', (e) => {
.on('error', e => {
callback(e);
});
@ -79,20 +79,20 @@ class PollyTTS {
describeVoices(callback, credentials) {
this.credentials = credentials;
const qs = {
Engine: 'neural',
Engine: 'neural'
};
const opts = {
service: 'polly',
region: 'us-east-1',
path: `/v1/voices?${querystring.stringify(qs)}`,
signQuery: true,
signQuery: true
};
// you can also pass AWS credentials in explicitly (otherwise taken from process.env)
aws4.sign(opts, this.credentials);
https
.get(opts, (res) => {
.get(opts, res => {
if (res.statusCode !== 200) {
return callback(new Error(`Request Failed. Status Code: ${res.statusCode}`));
}
@ -107,7 +107,7 @@ class PollyTTS {
return undefined;
})
.on('error', (e) => {
.on('error', e => {
callback(e);
});

View file

@ -1,34 +1,37 @@
/* global settings,twitch,trovo, fs, settingsPath, ini, shell, options, axios */
const twitchAuthentication = () =>
new Promise((resolve) => {
new Promise(resolve => {
const http = require('http');
const redirectUri = 'http://localhost:1989/auth';
const scopes = ['chat:edit', 'chat:read'];
const scopes = ['chat:edit', 'chat:read', 'user:read:follows', 'user:read:subscriptions', 'channel:read:redemptions'];
const express = require('express');
let tempAuthServer = express();
const tempAuthServer = express();
const port = 1989;
const { parse: parseQueryString } = require('querystring');
tempAuthServer.use(function (req, res, next) {
console.log('1');
if (req.url !== '/auth') {
let token = parseQueryString(req.query.auth);
console.log(req.url);
const token = parseQueryString(req.query.auth);
console.log(req);
console.log(token);
settings.TWITCH.OAUTH_TOKEN = token['#access_token'];
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
resolve(token['#access_token']);
stopServer();
} else {
res.send(htmlString);
}
next();
});
function stopServer() {
tempAuthServer.close();
}
const htmlString = `
<!DOCTYPE html>
<html>
<!DOCTYPE html>
<html>
<head>
<title>Authentication</title>
</head>
@ -38,7 +41,6 @@ const twitchAuthentication = () =>
<input type="text" id="auth" name="auth"/>
</form>
</body>
</html>
<script>
function onSubmitComplete() {
close();
@ -48,22 +50,16 @@ const twitchAuthentication = () =>
document.auth.submit();
setTimeout(onSubmitComplete, 500);
</script>
</html>
`;
tempAuthServer.get('/auth', (req, res) => {
res.send(htmlString);
});
tempAuthServer.post('/auth', (req, res) => {
res.render('authentication', { name: req.body.name });
});
console.log('-1');
const server = http.createServer(tempAuthServer);
server.listen(port, () => {
const authURL = `https://id.twitch.tv/oauth2/authorize?client_id=${settings.TWITCH.CLIENT_ID}&redirect_uri=${encodeURIComponent(
redirectUri,
)}&response_type=token&scope=${scopes.join(' ')}`;
redirectUri
)}&response_type=token&scope=${scopes.join('+')}`;
shell.openExternal(authURL);
});
@ -72,32 +68,63 @@ const twitchAuthentication = () =>
}
});
function getTwitchUserId() {
// Get user Logo with access token
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/users`,
headers: { 'Client-ID': settings.TWITCH.CLIENT_ID, Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}` },
};
axios
.request(options)
.then((responseLogoUrl) => {
console.log(responseLogoUrl.data.data[0]);
settings.TWITCH.USERNAME = responseLogoUrl.data.data[0].display_name;
settings.TWITCH.USER_LOGO_URL = responseLogoUrl.data.data[0].profile_image_url;
settings.TWITCH.USER_ID = responseLogoUrl.data.data[0].id;
fs.writeFileSync(settingsPath, ini.stringify(settings));
})
.catch((error) => {
console.error(error);
});
}
function getTwitchOauthToken() {
return twitchAuthentication().then((res) => {
getTwitchUserId();
return twitchAuthentication().then(res => {
twitch.getTwitchUserId();
return res;
});
}
module.exports = { getTwitchOauthToken };
const trovoAuthentication = () =>
new Promise(resolve => {
const http = require('http');
const redirectUri = 'http://localhost:1989/auth';
const scopes = ['user_details_self', 'chat_send_self', 'send_to_my_channel', 'manage_messages'];
const express = require('express');
const tempAuthServer = express();
const port = 1989;
tempAuthServer.use(function (req, res) {
res.send(htmlString);
const token = req.query.access_token;
settings.TROVO.OAUTH_TOKEN = token;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
resolve(token);
stopServer();
});
const htmlString = `
<!DOCTYPE html>
<html>
<head>
<title>Authentication</title>
</head>
<body>
<h1>Authentication successful! You can close this window now.</h1>
</body>
</html>
`;
const server = http.createServer(tempAuthServer);
server.listen(port, () => {
const authURL = `https://open.trovo.live/page/login.html?client_id=${
settings.TROVO.CLIENT_ID
}&response_type=token&scope=${scopes.join('+')}&redirect_uri=${encodeURIComponent(redirectUri)}`;
const lol = shell.openExternal(authURL);
});
function stopServer() {
server.close(() => {});
}
});
function getTrovoOAuthToken() {
return trovoAuthentication().then(res => {
trovo.getTrovoUserId();
return res;
});
}
module.exports = { getTwitchOauthToken, getTrovoOAuthToken };

View file

@ -1,5 +1,7 @@
/* global settings, resourcesPath, sound, twitch, getLanguageProperties, addSingleTooltip, showChatMessage, languageObject, addVoiceService, internalVoices, ttsRequestCount, main, path, pythonPath, settingsPath, ipcRenderer */
const spawn = require('child_process').spawn;
var kill = require('kill-process-by-name');
const kill = require('kill-process-by-name');
let python;
async function getInstalledVoices() {
@ -12,7 +14,7 @@ async function getInstalledVoices() {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/voices`, { method: 'GET' });
if (response.ok) {
const responseData = await response.json();
console.log('Response:', responseData);
console.log('Voices:', responseData);
internalVoices = responseData;
} else {
console.error('Failed to send termination signal to Flask server.');
@ -21,12 +23,12 @@ async function getInstalledVoices() {
console.error('Error sending termination signal:', error);
}
let primaryVoice = document.querySelector('#primaryVoice');
let secondaryVoice = document.querySelector('#secondaryVoice');
const primaryVoice = document.querySelector('#primaryVoice');
const secondaryVoice = document.querySelector('#secondaryVoice');
function setVoicesinSelect(voiceSelect) {
const voices = Object.values(internalVoices.voices);
voices.forEach((voice) => {
voices.forEach(voice => {
const option = document.createElement('option');
option.classList.add('option');
@ -42,12 +44,286 @@ async function getInstalledVoices() {
secondaryVoice.value = settings.TTS.SECONDARY_VOICE;
}
// TODO: refactor
function setTranslatedUserMessage(message) {
const userMessage = document.getElementById(message.messageId);
const messageBox = userMessage.getElementsByClassName('msg-box')[0];
const languageElement = document.createElement('span');
languageElement.classList = `fi fi-${message.language.selectedLanguage.ISO3166} fis flag-icon user-flag`;
languageElement.setAttribute('tip', message.language.selectedLanguage.name);
userMessage.appendChild(languageElement);
addSingleTooltip(languageElement);
const translationHeader = document.createElement('div');
translationHeader.className = 'translation-header user';
translationHeader.innerText = 'Translation';
messageBox.appendChild(translationHeader);
const languageElement2 = document.createElement('span');
languageElement2.classList = `fi fi-${message.language.detectedLanguage.ISO3166} fis flag-icon user`;
languageElement2.setAttribute('tip', message.language.detectedLanguage.name);
addSingleTooltip(languageElement2);
messageBox.appendChild(languageElement2);
const translationMessage = document.createElement('div');
translationMessage.className = 'translation-message user';
translationMessage.innerText = message.translation;
messageBox.appendChild(translationMessage);
}
function setTranslatedMessage(message) {
// this determines if it is a message that is send by a user
const languageBox = document.getElementById(message.messageId).getElementsByClassName('language-icon flag-icon')[0];
// if (false) {
// twitch.sendMessage(
// `[${message.language.detectedLanguage.name} ${message.language.detectedLanguage.ISO639} > ${message.language.selectedLanguage.name} ${message.language.selectedLanguage.ISO639}] @${settings.TWITCH.USERNAME}: ${message.translation}`
// );
// return setTranslatedUserMessage(message);
// }
if (message.language.selectedLanguage.ISO639 !== message.language.detectedLanguage.ISO639) {
const messageBox = document.getElementById(message.messageId).getElementsByClassName('msg-box')[0];
languageBox.classList = `fi fi-${message.language.detectedLanguage.ISO3166} fis language-icon flag-icon`;
languageBox.setAttribute('tip', message.language.detectedLanguage.name);
const translationHeader = document.createElement('div');
translationHeader.className = 'translation-header';
translationHeader.innerText = 'Translation';
messageBox.appendChild(translationHeader);
const translationIcon = document.createElement('div');
translationIcon.className = 'translation-icon';
const languageElement = document.createElement('span');
languageElement.classList = `fi fi-${message.language.selectedLanguage.ISO3166} fis flag-icon`;
languageElement.setAttribute('tip', message.language.selectedLanguage.name);
addSingleTooltip(languageElement);
translationIcon.appendChild(languageElement);
messageBox.appendChild(translationIcon);
const translationMessage = document.createElement('div');
translationMessage.className = 'translation-message';
translationMessage.innerText = message.translation;
messageBox.appendChild(translationMessage);
// showChatMessage();
const messages = document.body.querySelectorAll('.msg-container');
const lastMessage = messages[messages.length - 1];
lastMessage.scrollIntoView({ block: 'end', behavior: 'smooth' });
}
console.log(message);
if (settings.LANGUAGE.OUTPUT_TO_TTS) {
sound.playVoice({
originalMessage: message.originalMessage,
filteredMessage: message.translation,
logoUrl: message.logoUrl,
username: message.username,
formattedMessage: message.formattedMessage,
language: message.language
});
}
return message.language.detectedLanguage;
}
async function getTranslatedMessage(message) {
// TODO: translate primary language to
console.log(message);
console.log(message.isPrimaryLanguage ? message.language.selectedLanguage.IETF : message.language.detectedLanguage.IETF);
const requestOptions = {
method: 'POST', // HTTP method
headers: {
'Content-Type': 'application/json; charset="utf-8"' // Specify the content type
},
body: JSON.stringify({
message: message.message,
language: message.isPrimaryLanguage ? message.language.selectedLanguage.IETF : message.language.detectedLanguage.IETF
}) // Convert the data to JSON and include it in the request body
};
try {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/translate`, requestOptions);
const responseData = await response.json();
if (response.ok) {
console.log('Translated message:', responseData);
if (settings.LANGUAGE.BROADCAST_TRANSLATION) {
twitch.sendMessage(
`[${message.language.detectedLanguage.name} ${message.language.detectedLanguage.ISO639} > ${message.language.selectedLanguage.name} ${message.language.selectedLanguage.ISO639}] @${message.username}: ${responseData.translation}`
);
}
setTranslatedMessage({
originalMessage: message.message,
translation: responseData.translation,
messageId: message.messageId,
language: message.language,
formattedMessage: message.formattedMessage,
username: message.username,
logoUrl: message.logoUrl
});
return message.language.detectedLanguage;
} else {
console.error(responseData);
if (responseData.code === 500) {
if (message.remainingDetectedLanguages.length > 0) {
message.language.detectedLanguage = getLanguageProperties(message.remainingDetectedLanguages[0]);
message.remainingDetectedLanguages.shift();
return getTranslatedMessage(message);
} else {
message.message = 'Error, Could not translate message';
message.language.detectedLanguage = getLanguageProperties('en-GB');
return getTranslatedMessage(message);
}
}
if (responseData.code === 429) {
message.language.detectedLanguage = getLanguageProperties('en-GB');
setTranslatedMessage({
originalMessage: message.message,
translation: 'Rate limit exceeded, please change translation service.',
messageId: message.messageId,
language: message.language,
formattedMessage: message.formattedMessage,
username: message.username,
logoUrl: message.logoUrl
});
}
}
} catch (error) {
console.error('Error sending termination signal:', error);
message.language.detectedLanguage = getLanguageProperties('en-GB');
setTranslatedMessage({
originalMessage: message.message,
translation: 'Error, could not translate message.',
messageId: message.messageId,
language: message.language,
formattedMessage: message.formattedMessage,
username: message.username,
logoUrl: message.logoUrl
});
}
}
async function filterLanguage(message) {
const selectedPrimaryLanguage = getLanguageProperties(settings.LANGUAGE.TRANSLATE_TO);
const selectedPrimaryLanguageIndex =
message.languages.indexOf(selectedPrimaryLanguage.ISO639) === -1 ? 99 : message.languages.indexOf(selectedPrimaryLanguage.ISO639);
const selectedSecondaryLanguage = getLanguageProperties(settings.TTS.SECONDARY_TTS_LANGUAGE);
const selectedSecondaryLanguageIndex =
message.languages.indexOf(selectedSecondaryLanguage.ISO639) === -1 ? 99 : message.languages.indexOf(selectedSecondaryLanguage.ISO639);
let detectedLanguage = '';
const remainingDetectedLanguages = [];
const detectedLanguages = message.languages.slice();
for (const [index, language] of detectedLanguages.entries()) {
detectedLanguage = getLanguageProperties(language);
if (detectedLanguage !== 'error') {
detectedLanguages.splice(index, 1);
break;
}
}
for (const [index, language] of detectedLanguages.entries()) {
const remainderLanguage = getLanguageProperties(language);
if (remainderLanguage !== 'error') {
remainingDetectedLanguages.push(remainderLanguage.IETF);
}
}
const language = selectedPrimaryLanguageIndex < selectedSecondaryLanguageIndex ? selectedPrimaryLanguage : detectedLanguage;
if (settings.LANGUAGE.TRANSLATE_TO !== 'none' && selectedPrimaryLanguage.ISO639 !== language.ISO639) {
// console.log('1');
console.log('hola');
getTranslatedMessage({
message: message.message,
messageId: message.messageId,
remainingDetectedLanguages,
isPrimaryLanguage: false,
language: {
selectedLanguage: selectedPrimaryLanguage,
detectedLanguage: detectedLanguage
},
username: message.username,
formattedMessage: message.formattedMessage,
logoUrl: message.logoUrl
});
} else {
console.log('adios');
// console.log('2');
setTranslatedMessage({
originalMessage: message.message,
translation: message.message,
messageId: message.messageId,
language: {
selectedLanguage: selectedPrimaryLanguage,
detectedLanguage: selectedPrimaryLanguage
},
formattedMessage: message.formattedMessage,
username: message.username,
logoUrl: message.logoUrl
});
// getTranslatedMessage({
// message: message.message,
// messageId: message.messageId,
// remainingDetectedLanguages: [],
// isPrimaryLanguage: true,
// language: {
// selectedLanguage: selectedSecondaryLanguage,
// detectedLanguage: selectedPrimaryLanguage
// },
// username: message.username,
// formattedMessage: message.formattedMessage,
// logoUrl: message.logoUrl
// });
}
return language;
}
async function getDetectedLanguage(message) {
if (!settings.LANGUAGE.USE_DETECTION) {
return;
}
const requestOptions = {
method: 'POST', // HTTP method
headers: {
'Content-Type': 'application/json' // Specify the content type
},
body: JSON.stringify({ message: message.message }) // Convert the data to JSON and include it in the request body
};
try {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/detect`, requestOptions);
if (response.ok) {
const responseData = await response.json();
console.log('Detected Languages:', responseData);
return await filterLanguage({
languages: responseData.languages,
message: message.message,
messageId: message.messageId,
username: message.username,
formattedMessage: message.formattedMessage
});
} else {
console.error('Failed to send termination signal to Flask server.');
}
} catch (error) {
console.error('Error sending termination signal:', error);
}
}
async function getBackendServerStatus() {
try {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/status`, { method: 'GET' });
if (response.ok) {
const responseData = await response.json();
console.log('Response:', responseData);
console.log('Status:', responseData);
} else {
console.error('Failed to send termination signal to Flask server.');
}
@ -59,12 +335,12 @@ async function getBackendServerStatus() {
function startSTT() {
const eventSource = new EventSource('http://127.0.0.1:9000/stream');
eventSource.addEventListener('message', (event) => {
eventSource.addEventListener('message', event => {
const result = event.data;
console.log(result); // Log the received data
});
eventSource.addEventListener('error', (event) => {
eventSource.addEventListener('error', event => {
console.error('EventSource failed:', event);
eventSource.close();
@ -81,16 +357,16 @@ async function getInternalTTSAudio(requestData) {
const requestOptions = {
method: 'POST', // HTTP method
headers: {
'Content-Type': 'application/json', // Specify the content type
'Content-Type': 'application/json' // Specify the content type
},
body: JSON.stringify(requestData), // Convert the data to JSON and include it in the request body
body: JSON.stringify(requestData) // Convert the data to JSON and include it in the request body
};
try {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/audio`, requestOptions);
if (response.ok) {
const responseData = await response.json();
console.log('Response:', responseData);
console.log('Audio:', responseData);
return ttsRequestCount;
} else {
console.error('Failed to send termination signal to Flask server.');
@ -101,32 +377,36 @@ async function getInternalTTSAudio(requestData) {
}
const createBackendServer = () =>
new Promise((resolve) => {
new Promise(resolve => {
if (main.isPackaged) {
python = spawn(path.join(pythonPath, './loquendoBot_backend.exe'), [settingsPath, 'prod']);
} else {
python = spawn('python', ['-u', path.join(pythonPath, './loquendoBot_backend.py'), settingsPath, 'dev']);
python = spawn('python', ['-u', path.join(resourcesPath, '../backend/loquendoBot_backend.py'), settingsPath, 'dev']);
}
// Capture the stdout of the Python process
python.stdout.on('data', (data) => {
python.stdout.on('data', data => {
console.info(`${data}`);
});
// Capture the stderr of the Python process
python.stderr.on('data', (data) => {
python.stderr.on('data', data => {
// console.error(`${data}`);
if (data.toString().startsWith('INFO:waitress:Serving on')) {
resolve('finished');
} else {
console.error(`${data}`);
resolve('finished'); // cannot get it to resolve with stdout
}
});
// Listen for the Python process to exit
python.on('close', (code) => {
python.on('close', code => {
console.log(`Python process exited with code ${code}`);
});
if (typeof python.pid !== 'number') {
console.log('failed');
} else {
console.log(`Spawned subprocess correctly!, PID = ${python.pid}`);
// console.log(`Spawned subprocess correctly!, PID = ${python.pid}`);
}
});
@ -135,7 +415,7 @@ async function initiateBackend() {
createBackendServer().then(() => {
getBackendServerStatus();
getInstalledVoices();
if (settings.STT.USE_STT) {
if (settings.STT.USE_STT && !settings.STT.LANGUAGE === '') {
startSTT();
}
});
@ -146,7 +426,7 @@ async function initiateBackend() {
initiateBackend();
//TODO: convert to restartServer function
// TODO: convert to restartServer function
ipcRenderer.on('quit-event', async () => {
try {
const response = await fetch(`http://127.0.0.1:${settings.GENERAL.PORT}/terminate`, { method: 'GET' });
@ -164,4 +444,4 @@ ipcRenderer.on('quit-event', async () => {
}
});
module.exports = { getInternalTTSAudio };
module.exports = { getInternalTTSAudio, getDetectedLanguage, getTranslatedMessage };

View file

@ -1,4 +1,6 @@
function getResponse() {
/* global messageTemplates,getLanguageProperties, backend, messageId emojiPicker, settings, getPostTime, showChatMessage, twitch */
async function getResponse() {
const userText = document.querySelector('#textInput').value;
// If nothing is written don't do anything
@ -6,42 +8,79 @@ function getResponse() {
return;
}
messageId++;
// Create chat message from received data
const article = document.createElement('article');
article.className = 'msg-container-user';
article.setAttribute('id', messageId);
article.className = 'msg-container user';
article.innerHTML = messageTemplates.userTemplate;
const userImg = article.querySelector('.icon-container-user > .user-img-user');
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = settings.TWITCH.USER_LOGO_URL;
}
const postTime = article.querySelector('.post-time-user');
const iconContainer = article.querySelector('.icon-container-user');
iconContainer.appendChild(postTime);
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = getPostTime();
}
const msg = article.querySelector('.msg-box-user');
article.appendChild(postTime);
const msg = article.querySelector('.msg-box');
if (msg) {
msg.innerText = userText;
}
await replaceChatMessageWithCustomEmojis(userText).then(data => {
msg.innerHTML = data;
// Appends the message to the main chat box (shows the message)
showChatMessage(article, true);
showChatMessage(article);
twitch.sendMessage(userText);
if (settings.LANGUAGE.SEND_TRANSLATION) {
const selectedLanguage = getLanguageProperties(settings.LANGUAGE.SEND_TRANSLATION_IN);
const detectedLanguage = getLanguageProperties(settings.LANGUAGE.SEND_TRANSLATION_OUT);
backend.getTranslatedMessage({
message: data,
messageId: messageId,
remainingDetectedLanguages: [],
language: {
selectedLanguage,
detectedLanguage
},
formattedMessage: data,
username: 'You',
logoUrl: settings.TWITCH.USER_LOGO_URL
});
}
// Empty input box after sending message
document.body.querySelector('#textInput').value = '';
});
}
}
const replaceChatMessageWithCustomEmojis = message =>
new Promise(resolve => {
const words = message.split(' ');
words.forEach(async word => {
if (word !== '') {
await emojiPicker.database.getEmojiByUnicodeOrName(word).then(data => {
if (data && data.name === word) {
const url = `<img class="emote" src="${data.url}" >`;
message = message.replace(word, url);
}
});
resolve(message);
}
});
});
// Function that will execute when you press 'enter' in the message box
document.body.querySelector('#textInput').addEventListener('keydown', (e) => {
document.body.querySelector('#textInput').addEventListener('keydown', e => {
if (e.which === 13) {
getResponse();
}
@ -87,27 +126,28 @@ const displayPanel = (panelSelectorClass, panelSelectorID, btnSelectorID) => {
btn.addEventListener(
'click',
(event) => {
event => {
event.stopPropagation();
panels.forEach((el) => {
panels.forEach(el => {
if (el === panel) return;
el.classList.remove('show');
});
if (panel.classList.contains('show')) {
} else {
if (!panel.classList.contains('show')) {
panel.classList.add('show');
}
},
{
capture: true,
},
capture: true
}
);
};
displayPanel('.OptionPanel', '#Configuration', '#btnConfiguration');
displayPanel('.OptionPanel', '#Logs', '#btnLogs');
displayPanel('.OptionPanel', '#BrowsersourceChat', '#btnBrowsersourceChat');
displayPanel('.OptionPanel', '#BrowsersourceChatBubble', '#btnBrowsersourceChatBubble');
displayPanel('.OptionPanel', '#BrowsersourceVtuber', '#btnBrowsersourceVtuber');
displayPanel('.OptionPanel', '#BrowsersourcePNGTuber', '#btnBrowsersourcePNGTuber');
displayPanel('.OptionPanel', '#FaceMask', '#btnFaceMask');
displayPanel('.OptionPanel', '#Chat', '#btnChat');
displayPanel('.OptionPanel', '#ThemeCreator', '#btnThemeCreator');
displayPanel('.OptionPanel', '#ChatCreator', '#btnChatCreator');
@ -120,26 +160,27 @@ const displayPanelX = (panelSelectorClass, panelSelectorID, btnSelectorID) => {
btn.addEventListener(
'click',
(event) => {
event => {
event.stopPropagation();
panels.forEach((el) => {
panels.forEach(el => {
if (el === panel) return;
el.classList.remove('item-active');
});
if (panel.classList.contains('item-active')) {
} else {
if (!panel.classList.contains('item-active')) {
panel.classList.add('item-active');
}
},
{
capture: true,
},
capture: true
}
);
};
displayPanelX('.item', '#btnChat', '#btnChat');
displayPanelX('.item', '#btnBrowsersourceChat', '#btnBrowsersourceChat');
displayPanelX('.item', '#btnBrowsersourceChatBubble', '#btnBrowsersourceChatBubble');
displayPanelX('.item', '#btnBrowsersourceVtuber', '#btnBrowsersourceVtuber');
displayPanelX('.item', '#btnBrowsersourcePNGTuber', '#btnBrowsersourcePNGTuber');
displayPanelX('.item', '#btnFaceMask', '#btnFaceMask');
displayPanelX('.item', '#btnLogs', '#btnLogs');
displayPanelX('.item', '#btnConfiguration', '#btnConfiguration');
displayPanelX('.item', '#btnThemeCreator', '#btnThemeCreator');
@ -148,3 +189,7 @@ displayPanelX('.item', '#btnChatCreator', '#btnChatCreator');
// #region Show/Hide Theme Creator
// #endregion
module.exports = {
replaceChatMessageWithCustomEmojis
};

102
src/js/dlive.js Normal file
View file

@ -0,0 +1,102 @@
/* global settings, bot,settingsPath, fs, ini, messageId, showChatMessage, messageTemplates, chat, addSingleTooltip,getPostTime */
const Dlive = require('dlivetv-api');
const bot = new Dlive(settings.DLIVE.API_KEY);
function setTrovoSendButton() {
const languageSelectContent = document.querySelector('.send-to-channel');
const option = document.createElement('div');
option.classList = 'language-select';
const checkbox = document.createElement('input');
checkbox.classList = 'checkbox';
checkbox.type = 'checkbox';
checkbox.id = 'SEND_CHAT_DLIVE';
option.appendChild(checkbox);
const label = document.createElement('label');
label.classList = 'toggle-small';
option.setAttribute('for', 'SEND_CHAT_DLIVE');
checkbox.checked = settings.TROVO.SEND_CHAT;
option.appendChild(label);
const network = document.createElement('img');
network.src = './images/dlive.png';
network.classList = 'emote';
option.appendChild(network);
option.addEventListener('click', () => {
checkbox.checked = !checkbox.checked;
settings.TROVO.SEND_CHAT = checkbox.checked;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
});
languageSelectContent.appendChild(option);
}
setTrovoSendButton();
bot.on('ChatText', message => {
// console.log(`[${msg.sender.displayname}]: ${msg.content}`);
displayDLiveMessage(message);
});
// bot.sendMessage('test').then(console.log).catch(console.log);
bot.on('ChatGift', msg => {
console.log(`${msg.sender.displayname} donated ${msg.amount} ${msg.gift}'s`);
});
bot.on('ChatHost', msg => {
console.log(`${msg.sender.displayname} coming in with that ${msg.viewer} viewer host!`);
});
bot.on('ChatFollow', msg => {
console.log(`New follower! ${msg.sender.displayname} has joined the party!`);
});
async function displayDLiveMessage(message) {
messageId++;
const article = document.createElement('article');
article.className = 'msg-container sender';
article.setAttribute('id', messageId);
article.innerHTML = messageTemplates.dliveTemplate;
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = message.sender.avatar;
userImg.setAttribute('tip', '');
}
addSingleTooltip(userImg);
const usernameHtml = article.querySelector('.username');
if (usernameHtml) {
usernameHtml.innerText = message.sender.displayname;
}
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = getPostTime();
}
article.appendChild(postTime);
const formattedMessage = article.querySelector('.msg-box');
if (formattedMessage) {
formattedMessage.innerHTML = message.content;
// message.message.forEach(entry => {
// if (entry.text) {
// formattedMessage.innerHTML += entry.text;
// } else {
// formattedMessage.innerHTML += `<img src="${entry.url}"/>`;
// }
// });
}
await chat.replaceChatMessageWithCustomEmojis(formattedMessage.innerHTML).then(data => {
formattedMessage.innerHTML = data;
showChatMessage(article);
});
}

View file

@ -1,3 +1,5 @@
/* global settings, addVoiceService, googleVoices */
function getGoogleVoices() {
if (!settings.GOOGLE.USE_GOOGLE) {
return;
@ -5,12 +7,12 @@ function getGoogleVoices() {
addVoiceService('Google');
let primaryVoice = document.querySelector('#primaryGoogleVoice');
let secondaryVoice = document.querySelector('#secondaryGoogleVoice');
const primaryVoice = document.querySelector('#primaryGoogleVoice');
const secondaryVoice = document.querySelector('#secondaryGoogleVoice');
function setVoicesinSelect(voiceSelect) {
const voices = Object.values(googleVoices);
voices.forEach((voice) => {
voices.forEach(voice => {
const option = document.createElement('option');
option.classList.add('option');

View file

@ -4,328 +4,331 @@
// *info page with credits, version and more info
const languages = {
acehnese: { IETF: 'ace-ID', 'ISO-639': 'ace' },
afrikaans: { IETF: 'af-ZA', 'ISO-639': 'af' },
akan: { IETF: 'ak-GH', 'ISO-639': 'ak' },
albanian: { IETF: 'sq-AL', 'ISO-639': 'sq' },
amharic: { IETF: 'am-ET', 'ISO-639': 'am' },
'antigua and barbuda creole english': { IETF: 'aig-AG', 'ISO-639': 'aig' },
arabic: { IETF: 'ar-SA', 'ISO-639': 'ar' },
'arabic egyptian': { IETF: 'ar-EG', 'ISO-639': 'ar' },
aragonese: { IETF: 'an-ES', 'ISO-639': 'an' },
armenian: { IETF: 'hy-AM', 'ISO-639': 'hy' },
assamese: { IETF: 'as-IN', 'ISO-639': 'as' },
asturian: { IETF: 'ast-ES', 'ISO-639': 'ast' },
'austrian german': { IETF: 'de-AT', 'ISO-639': 'de' },
awadhi: { IETF: 'awa-IN', 'ISO-639': 'awa' },
'ayacucho quechua': { IETF: 'quy-PE', 'ISO-639': 'quy' },
azerbaijani: { IETF: 'az-AZ', 'ISO-639': 'az' },
'bahamas creole english': { IETF: 'bah-BS', 'ISO-639': 'bah' },
bajan: { IETF: 'bjs-BB', 'ISO-639': 'bjs' },
balinese: { IETF: 'ban-ID', 'ISO-639': 'ban' },
'balkan gipsy': { IETF: 'rm-RO', 'ISO-639': 'rm' },
bambara: { IETF: 'bm-ML', 'ISO-639': 'bm' },
banjar: { IETF: 'bjn-ID', 'ISO-639': 'bjn' },
bashkir: { IETF: 'ba-RU', 'ISO-639': 'ba' },
basque: { IETF: 'eu-ES', 'ISO-639': 'eu' },
belarusian: { IETF: 'be-BY', 'ISO-639': 'be' },
'belgian french': { IETF: 'fr-BE', 'ISO-639': 'fr' },
bemba: { IETF: 'bem-ZM', 'ISO-639': 'bem' },
bengali: { IETF: 'bn-IN', 'ISO-639': 'bn' },
bhojpuri: { IETF: 'bho-IN', 'ISO-639': 'bho' },
bihari: { IETF: 'bh-IN', 'ISO-639': 'bh' },
bislama: { IETF: 'bi-VU', 'ISO-639': 'bi' },
borana: { IETF: 'gax-KE', 'ISO-639': 'gax' },
bosnian: { IETF: 'bs-BA', 'ISO-639': 'bs' },
'bosnian (cyrillic)': { IETF: 'bs-Cyrl-BA', 'ISO-639': 'bs' },
breton: { IETF: 'br-FR', 'ISO-639': 'br' },
buginese: { IETF: 'bug-ID', 'ISO-639': 'bug' },
bulgarian: { IETF: 'bg-BG', 'ISO-639': 'bg' },
burmese: { IETF: 'my-MM', 'ISO-639': 'my' },
catalan: { IETF: 'ca-ES', 'ISO-639': 'ca' },
'catalan valencian': { IETF: 'cav-ES', 'ISO-639': 'cav' },
cebuano: { IETF: 'ceb-PH', 'ISO-639': 'ceb' },
'central atlas tamazight': { IETF: 'tzm-MA', 'ISO-639': 'tzm' },
'central aymara': { IETF: 'ayr-BO', 'ISO-639': 'ayr' },
'central kanuri (latin script)': { IETF: 'knc-NG', 'ISO-639': 'knc' },
'chadian arabic': { IETF: 'shu-TD', 'ISO-639': 'shu' },
chamorro: { IETF: 'ch-GU', 'ISO-639': 'ch' },
cherokee: { IETF: 'chr-US', 'ISO-639': 'chr' },
chhattisgarhi: { IETF: 'hne-IN', 'ISO-639': 'hne' },
'chinese simplified': { IETF: 'zh-CN', 'ISO-639': 'zh' },
'chinese trad. (hong kong)': { IETF: 'zh-HK', 'ISO-639': 'zh' },
'chinese traditional': { IETF: 'zh-TW', 'ISO-639': 'zh' },
'chinese traditional macau': { IETF: 'zh-MO', 'ISO-639': 'zh' },
chittagonian: { IETF: 'ctg-BD', 'ISO-639': 'ctg' },
chokwe: { IETF: 'cjk-AO', 'ISO-639': 'cjk' },
'classical greek': { IETF: 'grc-GR', 'ISO-639': 'grc' },
'comorian ngazidja': { IETF: 'zdj-KM', 'ISO-639': 'zdj' },
coptic: { IETF: 'cop-EG', 'ISO-639': 'cop' },
'crimean tatar': { IETF: 'crh-RU', 'ISO-639': 'crh' },
'crioulo upper guinea': { IETF: 'pov-GW', 'ISO-639': 'pov' },
croatian: { IETF: 'hr-HR', 'ISO-639': 'hr' },
czech: { IETF: 'cs-CZ', 'ISO-639': 'cs' },
danish: { IETF: 'da-DK', 'ISO-639': 'da' },
dari: { IETF: 'prs-AF', 'ISO-639': 'prs' },
dimli: { IETF: 'diq-TR', 'ISO-639': 'diq' },
dutch: { IETF: 'nl-NL', 'ISO-639': 'nl' },
dyula: { IETF: 'dyu-CI', 'ISO-639': 'dyu' },
dzongkha: { IETF: 'dz-BT', 'ISO-639': 'dz' },
'eastern yiddish': { IETF: 'ydd-US', 'ISO-639': 'ydd' },
emakhuwa: { IETF: 'vmw-MZ', 'ISO-639': 'vmw' },
english: { IETF: 'en-GB', 'ISO-639': 'en' },
'english australia': { IETF: 'en-AU', 'ISO-639': 'en' },
'english canada': { IETF: 'en-CA', 'ISO-639': 'en' },
'english india': { IETF: 'en-IN', 'ISO-639': 'en' },
'english ireland': { IETF: 'en-IE', 'ISO-639': 'en' },
'english new zealand': { IETF: 'en-NZ', 'ISO-639': 'en' },
'english singapore': { IETF: 'en-SG', 'ISO-639': 'en' },
'english south africa': { IETF: 'en-ZA', 'ISO-639': 'en' },
'english us': { IETF: 'en-US', 'ISO-639': 'en' },
esperanto: { IETF: 'eo-EU', 'ISO-639': 'eo' },
estonian: { IETF: 'et-EE', 'ISO-639': 'et' },
ewe: { IETF: 'ee-GH', 'ISO-639': 'ee' },
fanagalo: { IETF: 'fn-FNG', 'ISO-639': 'fn' },
faroese: { IETF: 'fo-FO', 'ISO-639': 'fo' },
fijian: { IETF: 'fj-FJ', 'ISO-639': 'fj' },
filipino: { IETF: 'fil-PH', 'ISO-639': 'fil' },
finnish: { IETF: 'fi-FI', 'ISO-639': 'fi' },
flemish: { IETF: 'nl-BE', 'ISO-639': 'nl' },
fon: { IETF: 'fon-BJ', 'ISO-639': 'fon' },
french: { IETF: 'fr-FR', 'ISO-639': 'fr' },
'french canada': { IETF: 'fr-CA', 'ISO-639': 'fr' },
'french swiss': { IETF: 'fr-CH', 'ISO-639': 'fr' },
friulian: { IETF: 'fur-IT', 'ISO-639': 'fur' },
fula: { IETF: 'ff-FUL', 'ISO-639': 'ff' },
galician: { IETF: 'gl-ES', 'ISO-639': 'gl' },
gamargu: { IETF: 'mfi-NG', 'ISO-639': 'mfi' },
garo: { IETF: 'grt-IN', 'ISO-639': 'grt' },
georgian: { IETF: 'ka-GE', 'ISO-639': 'ka' },
german: { IETF: 'de-DE', 'ISO-639': 'de' },
gilbertese: { IETF: 'gil-KI', 'ISO-639': 'gil' },
glavda: { IETF: 'glw-NG', 'ISO-639': 'glw' },
greek: { IETF: 'el-GR', 'ISO-639': 'el' },
'grenadian creole english': { IETF: 'gcl-GD', 'ISO-639': 'gcl' },
guarani: { IETF: 'gn-PY', 'ISO-639': 'gn' },
gujarati: { IETF: 'gu-IN', 'ISO-639': 'gu' },
'guyanese creole english': { IETF: 'gyn-GY', 'ISO-639': 'gyn' },
'haitian creole french': { IETF: 'ht-HT', 'ISO-639': 'ht' },
'halh mongolian': { IETF: 'khk-MN', 'ISO-639': 'khk' },
hausa: { IETF: 'ha-NE', 'ISO-639': 'ha' },
hawaiian: { IETF: 'haw-US', 'ISO-639': 'haw' },
hebrew: { IETF: 'he-IL', 'ISO-639': 'he' },
higi: { IETF: 'hig-NG', 'ISO-639': 'hig' },
hiligaynon: { IETF: 'hil-PH', 'ISO-639': 'hil' },
'hill mari': { IETF: 'mrj-RU', 'ISO-639': 'mrj' },
hindi: { IETF: 'hi-IN', 'ISO-639': 'hi' },
hmong: { IETF: 'hmn-CN', 'ISO-639': 'hmn' },
hungarian: { IETF: 'hu-HU', 'ISO-639': 'hu' },
icelandic: { IETF: 'is-IS', 'ISO-639': 'is' },
'igbo ibo': { IETF: 'ibo-NG', 'ISO-639': 'ibo' },
'igbo ig': { IETF: 'ig-NG', 'ISO-639': 'ig' },
ilocano: { IETF: 'ilo-PH', 'ISO-639': 'ilo' },
indonesian: { IETF: 'id-ID', 'ISO-639': 'id' },
'inuktitut greenlandic': { IETF: 'kl-GL', 'ISO-639': 'kl' },
'irish gaelic': { IETF: 'ga-IE', 'ISO-639': 'ga' },
italian: { IETF: 'it-IT', 'ISO-639': 'it' },
'italian swiss': { IETF: 'it-CH', 'ISO-639': 'it' },
'jamaican creole english': { IETF: 'jam-JM', 'ISO-639': 'jam' },
japanese: { IETF: 'ja-JP', 'ISO-639': 'ja' },
javanese: { IETF: 'jv-ID', 'ISO-639': 'jv' },
jingpho: { IETF: 'kac-MM', 'ISO-639': 'kac' },
"k'iche'": { IETF: 'quc-GT', 'ISO-639': 'quc' },
'kabiy<69>': { IETF: 'kbp-TG', 'ISO-639': 'kbp' },
kabuverdianu: { IETF: 'kea-CV', 'ISO-639': 'kea' },
kabylian: { IETF: 'kab-DZ', 'ISO-639': 'kab' },
kalenjin: { IETF: 'kln-KE', 'ISO-639': 'kln' },
kamba: { IETF: 'kam-KE', 'ISO-639': 'kam' },
kannada: { IETF: 'kn-IN', 'ISO-639': 'kn' },
kanuri: { IETF: 'kr-KAU', 'ISO-639': 'kr' },
karen: { IETF: 'kar-MM', 'ISO-639': 'kar' },
'kashmiri (devanagari script)': { IETF: 'ks-IN', 'ISO-639': 'ks' },
'kashmiri (arabic script)': { IETF: 'kas-IN', 'ISO-639': 'kas' },
kazakh: { IETF: 'kk-KZ', 'ISO-639': 'kk' },
khasi: { IETF: 'kha-IN', 'ISO-639': 'kha' },
khmer: { IETF: 'km-KH', 'ISO-639': 'km' },
'kikuyu kik': { IETF: 'kik-KE', 'ISO-639': 'kik' },
'kikuyu ki': { IETF: 'ki-KE', 'ISO-639': 'ki' },
kimbundu: { IETF: 'kmb-AO', 'ISO-639': 'kmb' },
kinyarwanda: { IETF: 'rw-RW', 'ISO-639': 'rw' },
kirundi: { IETF: 'rn-BI', 'ISO-639': 'rn' },
kisii: { IETF: 'guz-KE', 'ISO-639': 'guz' },
kongo: { IETF: 'kg-CG', 'ISO-639': 'kg' },
konkani: { IETF: 'kok-IN', 'ISO-639': 'kok' },
korean: { IETF: 'ko-KR', 'ISO-639': 'ko' },
'northern kurdish': { IETF: 'kmr-TR', 'ISO-639': 'kmr' },
'kurdish sorani': { IETF: 'ckb-IQ', 'ISO-639': 'ckb' },
kyrgyz: { IETF: 'ky-KG', 'ISO-639': 'ky' },
lao: { IETF: 'lo-LA', 'ISO-639': 'lo' },
latgalian: { IETF: 'ltg-LV', 'ISO-639': 'ltg' },
latin: { IETF: 'la-XN', 'ISO-639': 'la' },
latvian: { IETF: 'lv-LV', 'ISO-639': 'lv' },
ligurian: { IETF: 'lij-IT', 'ISO-639': 'lij' },
limburgish: { IETF: 'li-NL', 'ISO-639': 'li' },
lingala: { IETF: 'ln-LIN', 'ISO-639': 'ln' },
lithuanian: { IETF: 'lt-LT', 'ISO-639': 'lt' },
lombard: { IETF: 'lmo-IT', 'ISO-639': 'lmo' },
'luba-kasai': { IETF: 'lua-CD', 'ISO-639': 'lua' },
luganda: { IETF: 'lg-UG', 'ISO-639': 'lg' },
luhya: { IETF: 'luy-KE', 'ISO-639': 'luy' },
luo: { IETF: 'luo-KE', 'ISO-639': 'luo' },
luxembourgish: { IETF: 'lb-LU', 'ISO-639': 'lb' },
maa: { IETF: 'mas-KE', 'ISO-639': 'mas' },
macedonian: { IETF: 'mk-MK', 'ISO-639': 'mk' },
magahi: { IETF: 'mag-IN', 'ISO-639': 'mag' },
maithili: { IETF: 'mai-IN', 'ISO-639': 'mai' },
malagasy: { IETF: 'mg-MG', 'ISO-639': 'mg' },
malay: { IETF: 'ms-MY', 'ISO-639': 'ms' },
malayalam: { IETF: 'ml-IN', 'ISO-639': 'ml' },
maldivian: { IETF: 'dv-MV', 'ISO-639': 'dv' },
maltese: { IETF: 'mt-MT', 'ISO-639': 'mt' },
mandara: { IETF: 'mfi-CM', 'ISO-639': 'mfi' },
manipuri: { IETF: 'mni-IN', 'ISO-639': 'mni' },
'manx gaelic': { IETF: 'gv-IM', 'ISO-639': 'gv' },
maori: { IETF: 'mi-NZ', 'ISO-639': 'mi' },
marathi: { IETF: 'mr-IN', 'ISO-639': 'mr' },
margi: { IETF: 'mrt-NG', 'ISO-639': 'mrt' },
mari: { IETF: 'mhr-RU', 'ISO-639': 'mhr' },
marshallese: { IETF: 'mh-MH', 'ISO-639': 'mh' },
mende: { IETF: 'men-SL', 'ISO-639': 'men' },
meru: { IETF: 'mer-KE', 'ISO-639': 'mer' },
mijikenda: { IETF: 'nyf-KE', 'ISO-639': 'nyf' },
minangkabau: { IETF: 'min-ID', 'ISO-639': 'min' },
mizo: { IETF: 'lus-IN', 'ISO-639': 'lus' },
mongolian: { IETF: 'mn-MN', 'ISO-639': 'mn' },
montenegrin: { IETF: 'sr-ME', 'ISO-639': 'sr' },
morisyen: { IETF: 'mfe-MU', 'ISO-639': 'mfe' },
'moroccan arabic': { IETF: 'ar-MA', 'ISO-639': 'ar' },
mossi: { IETF: 'mos-BF', 'ISO-639': 'mos' },
ndau: { IETF: 'ndc-MZ', 'ISO-639': 'ndc' },
ndebele: { IETF: 'nr-ZA', 'ISO-639': 'nr' },
nepali: { IETF: 'ne-NP', 'ISO-639': 'ne' },
'nigerian fulfulde': { IETF: 'fuv-NG', 'ISO-639': 'fuv' },
niuean: { IETF: 'niu-NU', 'ISO-639': 'niu' },
'north azerbaijani': { IETF: 'azj-AZ', 'ISO-639': 'azj' },
sesotho: { IETF: 'nso-ZA', 'ISO-639': 'nso' },
'northern uzbek': { IETF: 'uzn-UZ', 'ISO-639': 'uzn' },
'norwegian bokm<6B>l': { IETF: 'nb-NO', 'ISO-639': 'nb' },
'norwegian nynorsk': { IETF: 'nn-NO', 'ISO-639': 'nn' },
nuer: { IETF: 'nus-SS', 'ISO-639': 'nus' },
nyanja: { IETF: 'ny-MW', 'ISO-639': 'ny' },
occitan: { IETF: 'oc-FR', 'ISO-639': 'oc' },
'occitan aran': { IETF: 'oc-ES', 'ISO-639': 'oc' },
odia: { IETF: 'or-IN', 'ISO-639': 'or' },
oriya: { IETF: 'ory-IN', 'ISO-639': 'ory' },
urdu: { IETF: 'ur-PK', 'ISO-639': 'ur' },
palauan: { IETF: 'pau-PW', 'ISO-639': 'pau' },
pali: { IETF: 'pi-IN', 'ISO-639': 'pi' },
pangasinan: { IETF: 'pag-PH', 'ISO-639': 'pag' },
papiamentu: { IETF: 'pap-CW', 'ISO-639': 'pap' },
pashto: { IETF: 'ps-PK', 'ISO-639': 'ps' },
persian: { IETF: 'fa-IR', 'ISO-639': 'fa' },
pijin: { IETF: 'pis-SB', 'ISO-639': 'pis' },
'plateau malagasy': { IETF: 'plt-MG', 'ISO-639': 'plt' },
polish: { IETF: 'pl-PL', 'ISO-639': 'pl' },
portuguese: { IETF: 'pt-PT', 'ISO-639': 'pt' },
'portuguese brazil': { IETF: 'pt-BR', 'ISO-639': 'pt' },
potawatomi: { IETF: 'pot-US', 'ISO-639': 'pot' },
punjabi: { IETF: 'pa-IN', 'ISO-639': 'pa' },
'punjabi (pakistan)': { IETF: 'pnb-PK', 'ISO-639': 'pnb' },
quechua: { IETF: 'qu-PE', 'ISO-639': 'qu' },
rohingya: { IETF: 'rhg-MM', 'ISO-639': 'rhg' },
rohingyalish: { IETF: 'rhl-MM', 'ISO-639': 'rhl' },
romanian: { IETF: 'ro-RO', 'ISO-639': 'ro' },
romansh: { IETF: 'roh-CH', 'ISO-639': 'roh' },
rundi: { IETF: 'run-BI', 'ISO-639': 'run' },
russian: { IETF: 'ru-RU', 'ISO-639': 'ru' },
'saint lucian creole french': { IETF: 'acf-LC', 'ISO-639': 'acf' },
samoan: { IETF: 'sm-WS', 'ISO-639': 'sm' },
sango: { IETF: 'sg-CF', 'ISO-639': 'sg' },
sanskrit: { IETF: 'sa-IN', 'ISO-639': 'sa' },
santali: { IETF: 'sat-IN', 'ISO-639': 'sat' },
sardinian: { IETF: 'sc-IT', 'ISO-639': 'sc' },
'scots gaelic': { IETF: 'gd-GB', 'ISO-639': 'gd' },
sena: { IETF: 'seh-ZW', 'ISO-639': 'seh' },
'serbian cyrillic': { IETF: 'sr-Cyrl-RS', 'ISO-639': 'sr' },
'serbian latin': { IETF: 'sr-Latn-RS', 'ISO-639': 'sr' },
'seselwa creole french': { IETF: 'crs-SC', 'ISO-639': 'crs' },
'setswana (south africa)': { IETF: 'tn-ZA', 'ISO-639': 'tn' },
shan: { IETF: 'shn-MM', 'ISO-639': 'shn' },
shona: { IETF: 'sn-ZW', 'ISO-639': 'sn' },
sicilian: { IETF: 'scn-IT', 'ISO-639': 'scn' },
silesian: { IETF: 'szl-PL', 'ISO-639': 'szl' },
'sindhi snd': { IETF: 'snd-PK', 'ISO-639': 'snd' },
'sindhi sd': { IETF: 'sd-PK', 'ISO-639': 'sd' },
sinhala: { IETF: 'si-LK', 'ISO-639': 'si' },
slovak: { IETF: 'sk-SK', 'ISO-639': 'sk' },
slovenian: { IETF: 'sl-SI', 'ISO-639': 'sl' },
somali: { IETF: 'so-SO', 'ISO-639': 'so' },
'sotho southern': { IETF: 'st-LS', 'ISO-639': 'st' },
'south azerbaijani': { IETF: 'azb-AZ', 'ISO-639': 'azb' },
'southern pashto': { IETF: 'pbt-PK', 'ISO-639': 'pbt' },
'southwestern dinka': { IETF: 'dik-SS', 'ISO-639': 'dik' },
spanish: { IETF: 'es-ES', 'ISO-639': 'es' },
'spanish argentina': { IETF: 'es-AR', 'ISO-639': 'es' },
'spanish colombia': { IETF: 'es-CO', 'ISO-639': 'es' },
'spanish latin america': { IETF: 'es-419', 'ISO-639': 'es' },
'spanish mexico': { IETF: 'es-MX', 'ISO-639': 'es' },
'spanish united states': { IETF: 'es-US', 'ISO-639': 'es' },
'sranan tongo': { IETF: 'srn-SR', 'ISO-639': 'srn' },
'standard latvian': { IETF: 'lvs-LV', 'ISO-639': 'lvs' },
'standard malay': { IETF: 'zsm-MY', 'ISO-639': 'zsm' },
sundanese: { IETF: 'su-ID', 'ISO-639': 'su' },
swahili: { IETF: 'sw-KE', 'ISO-639': 'sw' },
swati: { IETF: 'ss-SZ', 'ISO-639': 'ss' },
swedish: { IETF: 'sv-SE', 'ISO-639': 'sv' },
'swiss german': { IETF: 'de-CH', 'ISO-639': 'de' },
'syriac (aramaic)': { IETF: 'syc-TR', 'ISO-639': 'syc' },
tagalog: { IETF: 'tl-PH', 'ISO-639': 'tl' },
tahitian: { IETF: 'ty-PF', 'ISO-639': 'ty' },
tajik: { IETF: 'tg-TJ', 'ISO-639': 'tg' },
'tamashek (tuareg)': { IETF: 'tmh-DZ', 'ISO-639': 'tmh' },
tamasheq: { IETF: 'taq-ML', 'ISO-639': 'taq' },
'tamil india': { IETF: 'ta-IN', 'ISO-639': 'ta' },
'tamil sri lanka': { IETF: 'ta-LK', 'ISO-639': 'ta' },
taroko: { IETF: 'trv-TW', 'ISO-639': 'trv' },
tatar: { IETF: 'tt-RU', 'ISO-639': 'tt' },
telugu: { IETF: 'te-IN', 'ISO-639': 'te' },
tetum: { IETF: 'tet-TL', 'ISO-639': 'tet' },
thai: { IETF: 'th-TH', 'ISO-639': 'th' },
tibetan: { IETF: 'bo-CN', 'ISO-639': 'bo' },
tigrinya: { IETF: 'ti-ET', 'ISO-639': 'ti' },
'tok pisin': { IETF: 'tpi-PG', 'ISO-639': 'tpi' },
tokelauan: { IETF: 'tkl-TK', 'ISO-639': 'tkl' },
tongan: { IETF: 'to-TO', 'ISO-639': 'to' },
'tosk albanian': { IETF: 'als-AL', 'ISO-639': 'als' },
tsonga: { IETF: 'ts-ZA', 'ISO-639': 'ts' },
tswa: { IETF: 'tsc-MZ', 'ISO-639': 'tsc' },
tswana: { IETF: 'tn-BW', 'ISO-639': 'tn' },
tumbuka: { IETF: 'tum-MW', 'ISO-639': 'tum' },
turkish: { IETF: 'tr-TR', 'ISO-639': 'tr' },
turkmen: { IETF: 'tk-TM', 'ISO-639': 'tk' },
tuvaluan: { IETF: 'tvl-TV', 'ISO-639': 'tvl' },
twi: { IETF: 'tw-GH', 'ISO-639': 'tw' },
udmurt: { IETF: 'udm-RU', 'ISO-639': 'udm' },
ukrainian: { IETF: 'uk-UA', 'ISO-639': 'uk' },
uma: { IETF: 'ppk-ID', 'ISO-639': 'ppk' },
umbundu: { IETF: 'umb-AO', 'ISO-639': 'umb' },
'uyghur uig': { IETF: 'uig-CN', 'ISO-639': 'uig' },
'uyghur ug': { IETF: 'ug-CN', 'ISO-639': 'ug' },
uzbek: { IETF: 'uz-UZ', 'ISO-639': 'uz' },
venetian: { IETF: 'vec-IT', 'ISO-639': 'vec' },
vietnamese: { IETF: 'vi-VN', 'ISO-639': 'vi' },
'vincentian creole english': { IETF: 'svc-VC', 'ISO-639': 'svc' },
'virgin islands creole english': { IETF: 'vic-US', 'ISO-639': 'vic' },
wallisian: { IETF: 'wls-WF', 'ISO-639': 'wls' },
'waray (philippines)': { IETF: 'war-PH', 'ISO-639': 'war' },
welsh: { IETF: 'cy-GB', 'ISO-639': 'cy' },
'west central oromo': { IETF: 'gaz-ET', 'ISO-639': 'gaz' },
'western persian': { IETF: 'pes-IR', 'ISO-639': 'pes' },
wolof: { IETF: 'wo-SN', 'ISO-639': 'wo' },
xhosa: { IETF: 'xh-ZA', 'ISO-639': 'xh' },
yiddish: { IETF: 'yi-YD', 'ISO-639': 'yi' },
yoruba: { IETF: 'yo-NG', 'ISO-639': 'yo' },
zulu: { IETF: 'zu-ZA', 'ISO-639': 'zu' },
none: { IETF: 'none', ISO639: 'none', ISO3166: 'xx' },
english: { IETF: 'en-GB', ISO639: 'en', ISO3166: 'gb' },
spanish: { IETF: 'es-ES', ISO639: 'es', ISO3166: 'es' },
dutch: { IETF: 'nl-NL', ISO639: 'nl', ISO3166: 'nl' },
'chinese simplified': { IETF: 'zh-CN', ISO639: 'zh', ISO3166: 'cn' },
russian: { IETF: 'ru-RU', ISO639: 'ru', ISO3166: 'ru' },
indonesian: { IETF: 'id-ID', ISO639: 'id', ISO3166: 'id' },
hindi: { IETF: 'hi-IN', ISO639: 'hi', ISO3166: 'in' },
filipino: { IETF: 'fil-PH', ISO639: 'fil', ISO3166: 'ph' },
turkish: { IETF: 'tr-TR', ISO639: 'tr', ISO3166: 'tr' },
acehnese: { IETF: 'ace-ID', ISO639: 'ace', ISO3166: 'id' },
afrikaans: { IETF: 'af-ZA', ISO639: 'af', ISO3166: 'za' },
akan: { IETF: 'ak-GH', ISO639: 'ak', ISO3166: 'gh' },
albanian: { IETF: 'sq-AL', ISO639: 'sq', ISO3166: 'al' },
amharic: { IETF: 'am-ET', ISO639: 'am', ISO3166: 'et' },
'antigua and barbuda creole english': { IETF: 'aig-AG', ISO639: 'aig', ISO3166: 'ag' },
arabic: { IETF: 'ar-SA', ISO639: 'ar', ISO3166: 'sa' },
'arabic egyptian': { IETF: 'ar-EG', ISO639: 'arz', ISO3166: 'eg' },
aragonese: { IETF: 'es-ES', ISO639: 'an', ISO3166: 'es' },
armenian: { IETF: 'hy-AM', ISO639: 'hy', ISO3166: 'am' },
assamese: { IETF: 'as-IN', ISO639: 'as', ISO3166: 'in' },
asturian: { IETF: 'ast-ES', ISO639: 'ast', ISO3166: 'es' },
'austrian german': { IETF: 'de-AT', ISO639: 'de', ISO3166: 'at' },
awadhi: { IETF: 'awa-IN', ISO639: 'awa', ISO3166: 'in' },
'ayacucho quechua': { IETF: 'quy-PE', ISO639: 'quy', ISO3166: 'pe' },
azerbaijani: { IETF: 'az-AZ', ISO639: 'az', ISO3166: 'az' },
'bahamas creole english': { IETF: 'bah-BS', ISO639: 'bah', ISO3166: 'bs' },
bajan: { IETF: 'bjs-BB', ISO639: 'bjs', ISO3166: 'bb' },
balinese: { IETF: 'ban-ID', ISO639: 'ban', ISO3166: 'id' },
'balkan gipsy': { IETF: 'rm-RO', ISO639: 'rm', ISO3166: 'ro' },
bambara: { IETF: 'bm-ML', ISO639: 'bm', ISO3166: 'ml' },
banjar: { IETF: 'bjn-ID', ISO639: 'bjn', ISO3166: 'id' },
bashkir: { IETF: 'ba-RU', ISO639: 'ba', ISO3166: 'ru' },
basque: { IETF: 'eu-ES', ISO639: 'eu', ISO3166: 'es-pv' },
belarusian: { IETF: 'be-BY', ISO639: 'be', ISO3166: 'by' },
'belgian french': { IETF: 'fr-BE', ISO639: 'fr', ISO3166: 'be' },
bemba: { IETF: 'bem-ZM', ISO639: 'bem', ISO3166: 'zm' },
bengali: { IETF: 'bn-IN', ISO639: 'bn', ISO3166: 'bd' },
bhojpuri: { IETF: 'bho-IN', ISO639: 'bho', ISO3166: 'in' },
bihari: { IETF: 'bh-IN', ISO639: 'bh', ISO3166: 'in' },
bislama: { IETF: 'bi-VU', ISO639: 'bi', ISO3166: 'vu' },
borana: { IETF: 'gax-KE', ISO639: 'gax', ISO3166: 'ke' },
bosnian: { IETF: 'bs-BA', ISO639: 'bs', ISO3166: 'ba' },
'bosnian (cyrillic)': { IETF: 'bs-Cyrl-BA', ISO639: 'bs', ISO3166: 'ba' },
breton: { IETF: 'br-FR', ISO639: 'br', ISO3166: 'fr' },
buginese: { IETF: 'bug-ID', ISO639: 'bug', ISO3166: 'id' },
bulgarian: { IETF: 'bg-BG', ISO639: 'bg', ISO3166: 'bg' },
burmese: { IETF: 'my-MM', ISO639: 'my', ISO3166: 'mm' },
catalan: { IETF: 'ca-ES', ISO639: 'ca', ISO3166: 'es' },
'catalan valencian': { IETF: 'cav-ES', ISO639: 'cav', ISO3166: 'es' },
cebuano: { IETF: 'ceb-PH', ISO639: 'ceb', ISO3166: 'ph' },
'central atlas tamazight': { IETF: 'tzm-MA', ISO639: 'tzm', ISO3166: 'ma' },
'central aymara': { IETF: 'ayr-BO', ISO639: 'ayr', ISO3166: 'bo' },
'central kanuri (latin script)': { IETF: 'knc-NG', ISO639: 'knc', ISO3166: 'ng' },
'chadian arabic': { IETF: 'shu-TD', ISO639: 'shu', ISO3166: 'td' },
chamorro: { IETF: 'ch-GU', ISO639: 'ch', ISO3166: 'gu' },
cherokee: { IETF: 'chr-US', ISO639: 'chr', ISO3166: 'us' },
chhattisgarhi: { IETF: 'hne-IN', ISO639: 'hne', ISO3166: 'in' },
'chinese trad. (hong kong)': { IETF: 'zh-HK', ISO639: 'zh', ISO3166: 'hk' },
'chinese traditional': { IETF: 'zh-TW', ISO639: 'zh', ISO3166: 'tw' },
'chinese traditional macau': { IETF: 'zh-MO', ISO639: 'zh', ISO3166: 'mo' },
chittagonian: { IETF: 'ctg-BD', ISO639: 'ctg', ISO3166: 'bd' },
chokwe: { IETF: 'cjk-AO', ISO639: 'cjk', ISO3166: 'ao' },
'classical greek': { IETF: 'grc-GR', ISO639: 'grc', ISO3166: 'gr' },
'comorian ngazidja': { IETF: 'zdj-KM', ISO639: 'zdj', ISO3166: 'km' },
coptic: { IETF: 'cop-EG', ISO639: 'cop', ISO3166: 'eg' },
'crimean tatar': { IETF: 'crh-RU', ISO639: 'crh', ISO3166: 'tr' },
'crioulo upper guinea': { IETF: 'pov-GW', ISO639: 'pov', ISO3166: 'gw' },
croatian: { IETF: 'hr-HR', ISO639: 'hr', ISO3166: 'hr' },
'serbo-croatian': { IETF: 'sr-Cyrl-RS', ISO639: 'sh', ISO3166: 'sr' },
czech: { IETF: 'cs-CZ', ISO639: 'cs', ISO3166: 'cz' },
danish: { IETF: 'da-DK', ISO639: 'da', ISO3166: 'dk' },
dari: { IETF: 'prs-AF', ISO639: 'prs', ISO3166: 'af' },
dimli: { IETF: 'diq-TR', ISO639: 'diq', ISO3166: 'tr' },
dyula: { IETF: 'dyu-CI', ISO639: 'dyu', ISO3166: 'ci' },
dzongkha: { IETF: 'dz-BT', ISO639: 'dz', ISO3166: 'bt' },
'eastern yiddish': { IETF: 'ydd-US', ISO639: 'ydd', ISO3166: 'il' },
emakhuwa: { IETF: 'vmw-MZ', ISO639: 'vmw', ISO3166: 'mz' },
'english australia': { IETF: 'en-AU', ISO639: 'en', ISO3166: 'au' },
'english canada': { IETF: 'en-CA', ISO639: 'en', ISO3166: 'ca' },
'english india': { IETF: 'en-IN', ISO639: 'en', ISO3166: 'in' },
'english ireland': { IETF: 'en-IE', ISO639: 'en', ISO3166: 'ie' },
'english new zealand': { IETF: 'en-NZ', ISO639: 'en', ISO3166: 'nz' },
'english singapore': { IETF: 'en-SG', ISO639: 'en', ISO3166: 'sg' },
'english south africa': { IETF: 'en-ZA', ISO639: 'en', ISO3166: 'za' },
'english us': { IETF: 'en-US', ISO639: 'en', ISO3166: 'us' },
esperanto: { IETF: 'eo-EU', ISO639: 'eo', ISO3166: 'eu' },
estonian: { IETF: 'et-EE', ISO639: 'et', ISO3166: 'ee' },
ewe: { IETF: 'ee-GH', ISO639: 'ee', ISO3166: 'gh' },
fanagalo: { IETF: 'fn-FNG', ISO639: 'fn', ISO3166: 'za' },
faroese: { IETF: 'fo-FO', ISO639: 'fo', ISO3166: 'fo' },
fijian: { IETF: 'fj-FJ', ISO639: 'fj', ISO3166: 'fj' },
finnish: { IETF: 'fi-FI', ISO639: 'fi', ISO3166: 'fi' },
flemish: { IETF: 'nl-BE', ISO639: 'nl', ISO3166: 'be' },
fon: { IETF: 'fon-BJ', ISO639: 'fon', ISO3166: 'bj' },
french: { IETF: 'fr-FR', ISO639: 'fr', ISO3166: 'fr' },
'french canada': { IETF: 'fr-CA', ISO639: 'fr', ISO3166: 'ca' },
'french swiss': { IETF: 'fr-CH', ISO639: 'fr', ISO3166: 'ch' },
friulian: { IETF: 'fur-IT', ISO639: 'fur', ISO3166: 'it' },
fula: { IETF: 'ff-FUL', ISO639: 'ff', ISO3166: 'cm' },
galician: { IETF: 'gl-ES', ISO639: 'gl', ISO3166: 'es-ga' },
gamargu: { IETF: 'mfi-NG', ISO639: 'mfi', ISO3166: 'ng' },
garo: { IETF: 'grt-IN', ISO639: 'grt', ISO3166: 'in' },
georgian: { IETF: 'ka-GE', ISO639: 'ka', ISO3166: 'ge' },
german: { IETF: 'de-DE', ISO639: 'de', ISO3166: 'de' },
'Low German': { IETF: 'nl-NL', ISO639: 'nds', ISO3166: 'nl' },
gilbertese: { IETF: 'gil-KI', ISO639: 'gil', ISO3166: 'ki' },
glavda: { IETF: 'glw-NG', ISO639: 'glw', ISO3166: 'ng' },
greek: { IETF: 'el-GR', ISO639: 'el', ISO3166: 'gr' },
'grenadian creole english': { IETF: 'gcl-GD', ISO639: 'gcl', ISO3166: 'gd' },
guarani: { IETF: 'gn-PY', ISO639: 'gn', ISO3166: 'py' },
gujarati: { IETF: 'gu-IN', ISO639: 'gu', ISO3166: 'in' },
'guyanese creole english': { IETF: 'gyn-GY', ISO639: 'gyn', ISO3166: 'gy' },
'haitian creole french': { IETF: 'ht-HT', ISO639: 'ht', ISO3166: 'ht' },
'halh mongolian': { IETF: 'khk-MN', ISO639: 'khk', ISO3166: 'mn' },
hausa: { IETF: 'ha-NE', ISO639: 'ha', ISO3166: 'ne' },
hawaiian: { IETF: 'haw-US', ISO639: 'haw', ISO3166: 'xx' },
hebrew: { IETF: 'he-IL', ISO639: 'he', ISO3166: 'il' },
higi: { IETF: 'hig-NG', ISO639: 'hig', ISO3166: 'ng' },
hiligaynon: { IETF: 'hil-PH', ISO639: 'hil', ISO3166: 'ph' },
'hill mari': { IETF: 'mrj-RU', ISO639: 'mrj', ISO3166: 'xx' },
hmong: { IETF: 'hmn-CN', ISO639: 'hmn', ISO3166: 'cn' },
hungarian: { IETF: 'hu-HU', ISO639: 'hu', ISO3166: 'hu' },
icelandic: { IETF: 'is-IS', ISO639: 'is', ISO3166: 'is' },
'igbo ibo': { IETF: 'ibo-NG', ISO639: 'ibo', ISO3166: 'ng' },
'igbo ig': { IETF: 'ig-NG', ISO639: 'ig', ISO3166: 'ng' },
ilocano: { IETF: 'ilo-PH', ISO639: 'ilo', ISO3166: 'ph' },
'inuktitut greenlandic': { IETF: 'kl-GL', ISO639: 'kl', ISO3166: 'gl' },
'irish gaelic': { IETF: 'ga-IE', ISO639: 'ga', ISO3166: 'ie' },
italian: { IETF: 'it-IT', ISO639: 'it', ISO3166: 'it' },
'italian swiss': { IETF: 'it-CH', ISO639: 'it', ISO3166: 'ch' },
'jamaican creole english': { IETF: 'jam-JM', ISO639: 'jam', ISO3166: 'jm' },
japanese: { IETF: 'ja-JP', ISO639: 'ja', ISO3166: 'jp' },
javanese: { IETF: 'jv-ID', ISO639: 'jv', ISO3166: 'id' },
jingpho: { IETF: 'kac-MM', ISO639: 'kac', ISO3166: 'mm' },
"k'iche'": { IETF: 'quc-GT', ISO639: 'quc', ISO3166: 'gt' },
kabiye: { IETF: 'kbp-TG', ISO639: 'kbp', ISO3166: 'tg' },
kabuverdianu: { IETF: 'kea-CV', ISO639: 'kea', ISO3166: 'cv' },
kabylian: { IETF: 'kab-DZ', ISO639: 'kab', ISO3166: 'dz' },
kalenjin: { IETF: 'kln-KE', ISO639: 'kln', ISO3166: 'ke' },
kamba: { IETF: 'kam-KE', ISO639: 'kam', ISO3166: 'ke' },
kannada: { IETF: 'kn-IN', ISO639: 'kn', ISO3166: 'in' },
kanuri: { IETF: 'kr-KAU', ISO639: 'kr', ISO3166: 'xx' },
karen: { IETF: 'kar-MM', ISO639: 'kar', ISO3166: 'mm' },
'kashmiri (devanagari script)': { IETF: 'ks-IN', ISO639: 'ks', ISO3166: 'in' },
'kashmiri (arabic script)': { IETF: 'kas-IN', ISO639: 'kas', ISO3166: 'in' },
kazakh: { IETF: 'kk-KZ', ISO639: 'kk', ISO3166: 'kz' },
khasi: { IETF: 'kha-IN', ISO639: 'kha', ISO3166: 'in' },
khmer: { IETF: 'km-KH', ISO639: 'km', ISO3166: 'kh' },
'kikuyu kik': { IETF: 'kik-KE', ISO639: 'kik', ISO3166: 'ke' },
'kikuyu ki': { IETF: 'ki-KE', ISO639: 'ki', ISO3166: 'ke' },
kimbundu: { IETF: 'kmb-AO', ISO639: 'kmb', ISO3166: 'ao' },
kinyarwanda: { IETF: 'rw-RW', ISO639: 'rw', ISO3166: 'rw' },
kirundi: { IETF: 'rn-BI', ISO639: 'rn', ISO3166: 'bi' },
kisii: { IETF: 'guz-KE', ISO639: 'guz', ISO3166: 'ke' },
kongo: { IETF: 'kg-CG', ISO639: 'kg', ISO3166: 'cg' },
konkani: { IETF: 'kok-IN', ISO639: 'kok', ISO3166: 'in' },
korean: { IETF: 'ko-KR', ISO639: 'ko', ISO3166: 'kr' },
'northern kurdish': { IETF: 'kmr-TR', ISO639: 'kmr', ISO3166: 'tr' },
'kurdish sorani': { IETF: 'ckb-IQ', ISO639: 'ckb', ISO3166: 'iq' },
kyrgyz: { IETF: 'ky-KG', ISO639: 'ky', ISO3166: 'kg' },
lao: { IETF: 'lo-LA', ISO639: 'lo', ISO3166: 'la' },
latgalian: { IETF: 'ltg-LV', ISO639: 'ltg', ISO3166: 'lv' },
latin: { IETF: 'la-XN', ISO639: 'la', ISO3166: 'xx' },
latvian: { IETF: 'lv-LV', ISO639: 'lv', ISO3166: 'lg' },
ligurian: { IETF: 'lij-IT', ISO639: 'lij', ISO3166: 'it' },
limburgish: { IETF: 'li-NL', ISO639: 'li', ISO3166: 'nl' },
lingala: { IETF: 'ln-LIN', ISO639: 'ln', ISO3166: 'cd' },
lithuanian: { IETF: 'lt-LT', ISO639: 'lt', ISO3166: 'lt' },
lombard: { IETF: 'lmo-IT', ISO639: 'lmo', ISO3166: 'it' },
'luba-kasai': { IETF: 'lua-CD', ISO639: 'lua', ISO3166: 'cd' },
luganda: { IETF: 'lg-UG', ISO639: 'lg', ISO3166: 'ug' },
luhya: { IETF: 'luy-KE', ISO639: 'luy', ISO3166: 'ke' },
luo: { IETF: 'luo-KE', ISO639: 'luo', ISO3166: 'ke' },
luxembourgish: { IETF: 'lb-LU', ISO639: 'lb', ISO3166: 'lu' },
maa: { IETF: 'mas-KE', ISO639: 'mas', ISO3166: 'ke' },
macedonian: { IETF: 'mk-MK', ISO639: 'mk', ISO3166: 'mk' },
magahi: { IETF: 'mag-IN', ISO639: 'mag', ISO3166: 'in' },
maithili: { IETF: 'mai-IN', ISO639: 'mai', ISO3166: 'in' },
malagasy: { IETF: 'mg-MG', ISO639: 'mg', ISO3166: 'mg' },
malay: { IETF: 'ms-MY', ISO639: 'ms', ISO3166: 'my' },
malayalam: { IETF: 'ml-IN', ISO639: 'ml', ISO3166: 'in' },
maldivian: { IETF: 'dv-MV', ISO639: 'dv', ISO3166: 'mv' },
maltese: { IETF: 'mt-MT', ISO639: 'mt', ISO3166: 'mt' },
mandara: { IETF: 'mfi-CM', ISO639: 'mfi', ISO3166: 'cm' },
manipuri: { IETF: 'mni-IN', ISO639: 'mni', ISO3166: 'in' },
'manx gaelic': { IETF: 'gv-IM', ISO639: 'gv', ISO3166: 'im' },
maori: { IETF: 'mi-NZ', ISO639: 'mi', ISO3166: 'nz' },
marathi: { IETF: 'mr-IN', ISO639: 'mr', ISO3166: 'in' },
margi: { IETF: 'mrt-NG', ISO639: 'mrt', ISO3166: 'ng' },
mari: { IETF: 'mhr-RU', ISO639: 'mhr', ISO3166: 'xx' },
marshallese: { IETF: 'mh-MH', ISO639: 'mh', ISO3166: 'mh' },
mende: { IETF: 'men-SL', ISO639: 'men', ISO3166: 'sl' },
meru: { IETF: 'mer-KE', ISO639: 'mer', ISO3166: 'ke' },
mijikenda: { IETF: 'nyf-KE', ISO639: 'nyf', ISO3166: 'ke' },
minangkabau: { IETF: 'min-ID', ISO639: 'min', ISO3166: 'id' },
mizo: { IETF: 'lus-IN', ISO639: 'lus', ISO3166: 'in' },
mongolian: { IETF: 'mn-MN', ISO639: 'mn', ISO3166: 'mn' },
montenegrin: { IETF: 'sr-ME', ISO639: 'sr', ISO3166: 'me' },
morisyen: { IETF: 'mfe-MU', ISO639: 'mfe', ISO3166: 'mu' },
'moroccan arabic': { IETF: 'ar-MA', ISO639: 'ar', ISO3166: 'ma' },
mossi: { IETF: 'mos-BF', ISO639: 'mos', ISO3166: 'bf' },
ndau: { IETF: 'ndc-MZ', ISO639: 'ndc', ISO3166: 'mz' },
ndebele: { IETF: 'nr-ZA', ISO639: 'nr', ISO3166: 'za' },
nepali: { IETF: 'ne-NP', ISO639: 'ne', ISO3166: 'np' },
'nigerian fulfulde': { IETF: 'fuv-NG', ISO639: 'fuv', ISO3166: 'ng' },
niuean: { IETF: 'niu-NU', ISO639: 'niu', ISO3166: 'nu' },
'north azerbaijani': { IETF: 'azj-AZ', ISO639: 'azj', ISO3166: 'az' },
sesotho: { IETF: 'nso-ZA', ISO639: 'nso', ISO3166: 'za' },
'northern uzbek': { IETF: 'uzn-UZ', ISO639: 'uzn', ISO3166: 'uz' },
'norwegian bokm<6B>l': { IETF: 'nb-NO', ISO639: 'nb', ISO3166: 'no' },
'norwegian nynorsk': { IETF: 'nn-NO', ISO639: 'nn', ISO3166: 'no' },
nuer: { IETF: 'nus-SS', ISO639: 'nus', ISO3166: 'ss' },
nyanja: { IETF: 'ny-MW', ISO639: 'ny', ISO3166: 'mw' },
occitan: { IETF: 'oc-FR', ISO639: 'oc', ISO3166: 'fr' },
'occitan aran': { IETF: 'oc-ES', ISO639: 'oc', ISO3166: 'es-ct' },
odia: { IETF: 'or-IN', ISO639: 'or', ISO3166: 'in' },
oriya: { IETF: 'ory-IN', ISO639: 'ory', ISO3166: 'in' },
urdu: { IETF: 'ur-PK', ISO639: 'ur', ISO3166: 'pk' },
palauan: { IETF: 'pau-PW', ISO639: 'pau', ISO3166: 'pw' },
pali: { IETF: 'pi-IN', ISO639: 'pi', ISO3166: 'in' },
pangasinan: { IETF: 'pag-PH', ISO639: 'pag', ISO3166: 'ph' },
papiamentu: { IETF: 'pap-CW', ISO639: 'pap', ISO3166: 'cw' },
pashto: { IETF: 'ps-PK', ISO639: 'ps', ISO3166: 'pk' },
persian: { IETF: 'fa-IR', ISO639: 'fa', ISO3166: 'ir' },
pijin: { IETF: 'pis-SB', ISO639: 'pis', ISO3166: 'sb' },
'plateau malagasy': { IETF: 'plt-MG', ISO639: 'plt', ISO3166: 'mg' },
polish: { IETF: 'pl-PL', ISO639: 'pl', ISO3166: 'pl' },
portuguese: { IETF: 'pt-PT', ISO639: 'pt', ISO3166: 'pt' },
'portuguese brazil': { IETF: 'pt-BR', ISO639: 'pt', ISO3166: 'br' },
potawatomi: { IETF: 'pot-US', ISO639: 'pot', ISO3166: 'us' },
punjabi: { IETF: 'pa-IN', ISO639: 'pa', ISO3166: 'in' },
'punjabi (pakistan)': { IETF: 'pnb-PK', ISO639: 'pnb', ISO3166: 'pk' },
quechua: { IETF: 'qu-PE', ISO639: 'qu', ISO3166: 'pe' },
rohingya: { IETF: 'rhg-MM', ISO639: 'rhg', ISO3166: 'mm' },
rohingyalish: { IETF: 'rhl-MM', ISO639: 'rhl', ISO3166: 'mm' },
romanian: { IETF: 'ro-RO', ISO639: 'ro', ISO3166: 'ro' },
romansh: { IETF: 'roh-CH', ISO639: 'roh', ISO3166: 'ch' },
rundi: { IETF: 'run-BI', ISO639: 'run', ISO3166: 'bi' },
'saint lucian creole french': { IETF: 'acf-LC', ISO639: 'acf', ISO3166: 'lc' },
samoan: { IETF: 'sm-WS', ISO639: 'sm', ISO3166: 'ws' },
sango: { IETF: 'sg-CF', ISO639: 'sg', ISO3166: 'cf' },
sanskrit: { IETF: 'sa-IN', ISO639: 'sa', ISO3166: 'in' },
santali: { IETF: 'sat-IN', ISO639: 'sat', ISO3166: 'in' },
sardinian: { IETF: 'sc-IT', ISO639: 'sc', ISO3166: 'it' },
'scots gaelic': { IETF: 'gd-GB', ISO639: 'gd', ISO3166: 'gb-sct' },
sena: { IETF: 'seh-ZW', ISO639: 'seh', ISO3166: 'zw' },
'serbian cyrillic': { IETF: 'sr-Cyrl-RS', ISO639: 'sr', ISO3166: 'rs' },
'serbian latin': { IETF: 'sr-Latn-RS', ISO639: 'sr', ISO3166: 'rs' },
'seselwa creole french': { IETF: 'crs-SC', ISO639: 'crs', ISO3166: 'sc' },
'setswana (south africa)': { IETF: 'tn-ZA', ISO639: 'tn', ISO3166: 'za' },
shan: { IETF: 'shn-MM', ISO639: 'shn', ISO3166: 'mm' },
shona: { IETF: 'sn-ZW', ISO639: 'sn', ISO3166: 'zw' },
sicilian: { IETF: 'scn-IT', ISO639: 'scn', ISO3166: 'it' },
silesian: { IETF: 'szl-PL', ISO639: 'szl', ISO3166: 'pl' },
'sindhi snd': { IETF: 'snd-PK', ISO639: 'snd', ISO3166: 'pk' },
'sindhi sd': { IETF: 'sd-PK', ISO639: 'sd', ISO3166: 'pk' },
sinhala: { IETF: 'si-LK', ISO639: 'si', ISO3166: 'lk' },
slovak: { IETF: 'sk-SK', ISO639: 'sk', ISO3166: 'sk' },
slovenian: { IETF: 'sl-SI', ISO639: 'sl', ISO3166: 'si' },
somali: { IETF: 'so-SO', ISO639: 'so', ISO3166: 'so' },
'sotho southern': { IETF: 'st-LS', ISO639: 'st', ISO3166: 'ls' },
'south azerbaijani': { IETF: 'azb-AZ', ISO639: 'azb', ISO3166: 'az' },
'southern pashto': { IETF: 'pbt-PK', ISO639: 'pbt', ISO3166: 'pk' },
'southwestern dinka': { IETF: 'dik-SS', ISO639: 'dik', ISO3166: 'ss' },
'spanish argentina': { IETF: 'es-AR', ISO639: 'es', ISO3166: 'ar' },
'spanish colombia': { IETF: 'es-CO', ISO639: 'es', ISO3166: 'co' },
'spanish latin america': { IETF: 'es-419', ISO639: 'es', ISO3166: 'do' },
'spanish mexico': { IETF: 'es-MX', ISO639: 'es', ISO3166: 'mx' },
'spanish united states': { IETF: 'es-US', ISO639: 'es', ISO3166: 'es' },
'sranan tongo': { IETF: 'srn-SR', ISO639: 'srn', ISO3166: 'sr' },
'standard latvian': { IETF: 'lvs-LV', ISO639: 'lvs', ISO3166: 'lv' },
'standard malay': { IETF: 'zsm-MY', ISO639: 'zsm', ISO3166: 'my' },
sundanese: { IETF: 'su-ID', ISO639: 'su', ISO3166: 'id' },
swahili: { IETF: 'sw-KE', ISO639: 'sw', ISO3166: 'ke' },
swati: { IETF: 'ss-SZ', ISO639: 'ss', ISO3166: 'sz' },
swedish: { IETF: 'sv-SE', ISO639: 'sv', ISO3166: 'se' },
'swiss german': { IETF: 'de-CH', ISO639: 'de', ISO3166: 'ch' },
'syriac (aramaic)': { IETF: 'syc-TR', ISO639: 'syc', ISO3166: 'tr' },
tagalog: { IETF: 'tl-PH', ISO639: 'tl', ISO3166: 'ph' },
tahitian: { IETF: 'ty-PF', ISO639: 'ty', ISO3166: 'pf' },
tajik: { IETF: 'tg-TJ', ISO639: 'tg', ISO3166: 'tj' },
'tamashek (tuareg)': { IETF: 'tmh-DZ', ISO639: 'tmh', ISO3166: 'dz' },
tamasheq: { IETF: 'taq-ML', ISO639: 'taq', ISO3166: 'ml' },
'tamil india': { IETF: 'ta-IN', ISO639: 'ta', ISO3166: 'in' },
'tamil sri lanka': { IETF: 'ta-LK', ISO639: 'ta', ISO3166: 'lk' },
taroko: { IETF: 'trv-TW', ISO639: 'trv', ISO3166: 'tw' },
tatar: { IETF: 'tt-RU', ISO639: 'tt', ISO3166: 'ru' },
telugu: { IETF: 'te-IN', ISO639: 'te', ISO3166: 'in' },
tetum: { IETF: 'tet-TL', ISO639: 'tet', ISO3166: 'tl' },
thai: { IETF: 'th-TH', ISO639: 'th', ISO3166: 'th' },
tibetan: { IETF: 'bo-CN', ISO639: 'bo', ISO3166: 'cn' },
tigrinya: { IETF: 'ti-ET', ISO639: 'ti', ISO3166: 'et' },
'tok pisin': { IETF: 'tpi-PG', ISO639: 'tpi', ISO3166: 'pg' },
tokelauan: { IETF: 'tkl-TK', ISO639: 'tkl', ISO3166: 'tk' },
tongan: { IETF: 'to-TO', ISO639: 'to', ISO3166: 'to' },
'tosk albanian': { IETF: 'als-AL', ISO639: 'als', ISO3166: 'al' },
tsonga: { IETF: 'ts-ZA', ISO639: 'ts', ISO3166: 'za' },
tswa: { IETF: 'tsc-MZ', ISO639: 'tsc', ISO3166: 'mz' },
tswana: { IETF: 'tn-BW', ISO639: 'tn', ISO3166: 'bw' },
tumbuka: { IETF: 'tum-MW', ISO639: 'tum', ISO3166: 'mw' },
turkmen: { IETF: 'tk-TM', ISO639: 'tk', ISO3166: 'tm' },
tuvaluan: { IETF: 'tvl-TV', ISO639: 'tvl', ISO3166: 'tv' },
twi: { IETF: 'tw-GH', ISO639: 'tw', ISO3166: 'gh' },
udmurt: { IETF: 'udm-RU', ISO639: 'udm', ISO3166: 'xx' },
ukrainian: { IETF: 'uk-UA', ISO639: 'uk', ISO3166: 'ua' },
uma: { IETF: 'ppk-ID', ISO639: 'ppk', ISO3166: 'id' },
umbundu: { IETF: 'umb-AO', ISO639: 'umb', ISO3166: 'ao' },
'uyghur uig': { IETF: 'uig-CN', ISO639: 'uig', ISO3166: 'cn' },
'uyghur ug': { IETF: 'ug-CN', ISO639: 'ug', ISO3166: 'cn' },
uzbek: { IETF: 'uz-UZ', ISO639: 'uz', ISO3166: 'uz' },
venetian: { IETF: 'vec-IT', ISO639: 'vec', ISO3166: 'it' },
vietnamese: { IETF: 'vi-VN', ISO639: 'vi', ISO3166: 'vn' },
'vincentian creole english': { IETF: 'svc-VC', ISO639: 'svc', ISO3166: 'vc' },
'virgin islands creole english': { IETF: 'vic-US', ISO639: 'vic', ISO3166: 'vi' },
wallisian: { IETF: 'wls-WF', ISO639: 'wls', ISO3166: 'wf' },
'waray (philippines)': { IETF: 'war-PH', ISO639: 'war', ISO3166: 'ph' },
welsh: { IETF: 'cy-GB', ISO639: 'cy', ISO3166: 'gb-wls' },
'west central oromo': { IETF: 'gaz-ET', ISO639: 'gaz', ISO3166: 'et' },
'western persian': { IETF: 'pes-IR', ISO639: 'pes', ISO3166: 'ir' },
wolof: { IETF: 'wo-SN', ISO639: 'wo', ISO3166: 'sn' },
xhosa: { IETF: 'xh-ZA', ISO639: 'xh', ISO3166: 'za' },
yiddish: { IETF: 'yi-YD', ISO639: 'yi', ISO3166: 'il' },
yoruba: { IETF: 'yo-NG', ISO639: 'yo', ISO3166: 'ng' },
zulu: { IETF: 'zu-ZA', ISO639: 'zu', ISO3166: 'za' }
};
module.exports = { languages };

View file

@ -9,13 +9,13 @@ const consoleFormat = format.combine(
format.colorize(),
format.timestamp(),
format.align(),
format.printf((info) => `${info.timestamp} - ${info.level}: ${info.message} ${JSON.stringify(info.metadata)}`),
format.printf(info => `${info.timestamp} - ${info.level}: ${info.message} ${JSON.stringify(info.metadata)}`)
);
const fileFormat = format.combine(
format.timestamp(),
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }),
format.json(),
format.json()
);
const logger = createLogger({
@ -24,32 +24,32 @@ const logger = createLogger({
transports: [
new transports.File({
filename: path.join(__dirname, '../logs/error.log'),
level: 'error',
level: 'error'
}),
new transports.File({
filename: path.join(__dirname, '../logs/activity.log'),
maxsize: 5242880,
maxFiles: 5,
}),
],
maxFiles: 5
})
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(
new transports.Console({
level: consoleloggerLevel,
format: consoleFormat,
}),
format: consoleFormat
})
);
}
fetch(path.join(__dirname, '../logs/activity.log'))
.then((response) => response.text())
.then((logData) => {
.then(response => response.text())
.then(logData => {
const logLines = logData.trim().split('\n');
const tableBody = document.getElementById('logContent');
logLines.forEach((logLine) => {
logLines.forEach(logLine => {
const logObject = JSON.parse(logLine);
const row = document.createElement('tr');
@ -73,6 +73,8 @@ fetch(path.join(__dirname, '../logs/activity.log'))
tableBody.appendChild(row);
});
})
.catch((error) => {});
.catch(error => {
console.error(error);
});
module.exports = logger;

View file

@ -1,15 +1,17 @@
let micSelect = document.querySelector('#microphone');
/* global settings, */
const micSelect = document.querySelector('#microphone');
let selectedMic;
function getAvailableMediaDevices(type) {
return new Promise((resolve, reject) => {
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
const microphones = devices.filter((device) => device.kind === type);
.then(devices => {
const microphones = devices.filter(device => device.kind === type);
resolve(microphones);
})
.catch((error) => {
.catch(error => {
reject(error);
});
});
@ -17,10 +19,10 @@ function getAvailableMediaDevices(type) {
// Microphones
getAvailableMediaDevices('audioinput')
.then((microphones) => {
.then(microphones => {
let i = 0;
let tempname = '';
for (let mic of microphones) {
for (const mic of microphones) {
if (mic.deviceId === 'default') {
tempname = mic.label.slice(10); // remove "default -" from the label to get the default device name.
}
@ -44,6 +46,6 @@ getAvailableMediaDevices('audioinput')
i++;
}
})
.catch((error) => {
.catch(error => {
console.error('Error retrieving microphones:', error);
});

View file

@ -1,42 +1,51 @@
const twitchTemplate = `
<div class="icon-container">
<img class="user-img" src="" />
<img class="status-circle" src="./images/twitch-icon.png" />
</div>
<span class="username"></span>
<div class="msg-box">
</div>
<img class="status-circle sender" src="./images/twitch-icon.png" tip="Twitch" />
<span class="post-time sender"></span>
<span class="username sender"></span>
<div class="msg-box sender"></div>
`.trim();
const trovoTemplate = `
<img class="user-img" src="" />
<img class="status-circle sender" src="./images/trovo-icon.jpg" tip="Trovo" />
<span class="post-time sender"></span>
<span class="username sender"></span>
<div class="msg-box sender"></div>
`.trim();
const youtubeTemplate = `
<img class="user-img" src="" />
<img class="status-circle sender" src="./images/youtube-icon.png" tip="Youtube" />
<span class="post-time sender"></span>
<span class="username sender"></span>
<div class="msg-box sender"></div>
`.trim();
const dliveTemplate = `
<img class="user-img" src="" />
<img class="status-circle sender" src="./images/dlive-icon.png" tip="DLive" />
<span class="post-time sender"></span>
<span class="username sender"></span>
<div class="msg-box sender"></div>
`.trim();
const userTemplate = `
<div class="icon-container-user">
<span class="post-time-user">You</span>
<img class="status-circle-user" src="./images/twitch-icon.png" />
<img class="user-img-user" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
</div>
<span class="username-user">You</span>
<div class="msg-box-user">
</div>
<img class="user-img" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
<img class="status-circle user" src="./images/twitch-icon.png" />
<span class="post-time user"></span>
<span class="username user">You</span>
<div class="msg-box user"></div>
`.trim();
const messageTemplate = `
<article class="msg-container msg-self" id="msg-0">
<div class="icon-container-user">
<img class="user-img-user" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
<img class="status-circle-user" src="./images/twitch-icon.png" />
</div>
<div class="msg-box-user msg-box-user-temp">
<div class="flr">
<div class="messages-user">
<span class="timestamp timestamp-temp"><span class="username username-temp">You</span><span class="posttime">${getPostTime()}</span></span>
<br>
<p class="msg msg-temp" id="msg-0">
hello there
</p>
</div>
</div>
</div>
<article class=" user">
<img class="user-img" src="https://gravatar.com/avatar/56234674574535734573000000000001?d=retro" />
<img class="status-circle user" src="./images/twitch-icon.png" />
<span class="post-time user"> 12:00 PM</span>
<span class="username user">You</span>
<div class="msg-box user">Hello there</div>
</article>
`.trim();
module.exports = { twitchTemplate, userTemplate, messageTemplate };
module.exports = { twitchTemplate, dliveTemplate, youtubeTemplate, userTemplate, messageTemplate, trovoTemplate };

View file

@ -1,14 +1,12 @@
/* eslint-disable no-unused-vars */
const fs = require('fs');
const ini = require('ini');
const path = require('path'); // get directory path
const path = require('path');
const axios = require('axios');
const Sockette = require('sockette');
const { ipcRenderer, shell } = require('electron'); // necessary electron libraries to send data to the app
const { webFrame, ipcRenderer, shell } = require('electron');
const io = require('socket.io-client');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const GoogleTTS = require('node-google-tts-api');
const tts = new GoogleTTS();
@ -17,14 +15,16 @@ const { Socket } = require('socket.io-client');
const main = ipcRenderer.sendSync('environment');
const resourcesPath = main.resourcesPath;
let settingsPath = main.settingsPath.toString();
let pythonPath = main.pythonPath.toString();
const settingsPath = main.settingsPath.toString();
const pythonPath = main.pythonPath.toString();
const settings = main.settings;
// TODO: remove gooogle voices txt and use api instead
const googleVoices = fs.readFileSync(path.join(__dirname, './config/googleVoices.txt')).toString().split('\r\n');
// TODO: remove amazon voices txt and use api instead (sakura project has it)
const amazonVoices = fs.readFileSync(path.join(__dirname, './config/amazonVoices.txt')).toString().split('\r\n');
const customEmotesListSavePath =
main.isPackaged === true ? path.join(resourcesPath, './custom-emotes.json') : path.join(resourcesPath, './config/custom-emotes.json');
// html elements
const root = document.documentElement;
@ -35,6 +35,9 @@ const devicesDropdown = document.querySelector('#devicesDropdown');
const notificationSound = document.querySelector('#notification'); // obtain the html reference of the sound comboBox
const sttModel = document.querySelector('#sttModel'); // obtain the html reference of the sound comboBox
const ttsAudioDevices = document.querySelector('#ttsAudioDevice'); // obtain the html reference of the installedTTS comboBox
const notificationSoundAudioDevices = document.querySelector('#notificationSoundAudioDevice'); // obtain the html reference of the installedTTS comboBox
const emojiPicker = document.body.querySelector('emoji-picker');
const lol = document.body.querySelector('country-flag-emoji-polyfill');
// laod local javascript files
const chat = require(path.join(__dirname, './js/chat'));
@ -44,21 +47,43 @@ const languageObject = require(path.join(__dirname, './js/languages'));
const logger = require(path.join(__dirname, './js/logger'));
const sound = require(path.join(__dirname, './js/sound'));
const config = require(path.join(__dirname, './js/settings'));
const { TokenAutocomplete } = require(path.join(__dirname, './js/token-autocomplete'));
const betterTTVAutocomplete = new TokenAutocomplete({
name: 'sample',
selector: '#sample',
noMatchesText: 'No matching results...',
initialTokens: [...settings.TWITCH.BETTERTTV_CHANNELS]
});
const test = settings.TWITCH.BETTERTTV_CHANNELS;
console.log(test);
const mediaDevices = require(path.join(__dirname, './js/mediaDevices'));
let notificationSounds = path.join(__dirname, './sounds/notifications');
let sttModels = path.join(__dirname, '../speech_to_text_models');
const notificationSounds = path.join(resourcesPath, main.isPackaged ? './sounds/notifications' : '../sounds/notifications');
const sttModels = path.join(resourcesPath, main.isPackaged ? './speech_to_text_models' : '../speech_to_text_models');
function reset() {
ipcRenderer.send('restart');
}
let server = require(path.join(__dirname, './js/server'));
const server = require(path.join(__dirname, './js/server'));
const backend = require(path.join(__dirname, './js/backend'));
let socket = io(`http://localhost:${settings.GENERAL.PORT}`); // Connect to your Socket.IO server
const socket = io(`http://localhost:${settings.GENERAL.PORT}`); // Connect to your Socket.IO server
let twitch = null;
twitch = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './js/twitch')) : '';
let dlive = null;
dlive = settings.DLIVE.USE_DLIVE ? require(path.join(__dirname, './js/dlive')) : '';
let youtube = null;
youtube = settings.YOUTUBE.USE_YOUTUBE ? require(path.join(__dirname, './js/youtube')) : '';
let trovo = null;
trovo = settings.TROVO.USE_TROVO ? require(path.join(__dirname, './js/trovo')) : '';
let twitch = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './js/twitch')) : '';
const Polly = settings.AMAZON.USE_AMAZON ? require(path.join(__dirname, './js/amazon')) : '';
const google = settings.GOOGLE.USE_GOOGLE ? require(path.join(__dirname, './js/google')) : '';
@ -66,6 +91,12 @@ const theme = require(path.join(__dirname, './js/theme'));
const auth = require(path.join(__dirname, './js/auth'));
let ttsRequestCount = 0;
ttsRequestCount = 0;
let customEmojis = [];
customEmojis = [];
let messageId = 0;
messageId = 0;
let customEmojiList = [];
// initialize values
config.getGeneralSettings();
@ -78,11 +109,15 @@ const speakButton = document.querySelector('#speakBtn');
const amazonCredentials = {
accessKeyId: settings.AMAZON.ACCESS_KEY,
secretAccessKey: settings.AMAZON.ACCESS_SECRET,
secretAccessKey: settings.AMAZON.ACCESS_SECRET
};
// Check for installed sounds
fs.readdir(notificationSounds, (err, files) => {
if (err) {
console.error(err);
}
files.forEach((file, i) => {
// Create a new option element.
const option = document.createElement('option');
@ -101,7 +136,11 @@ fs.readdir(notificationSounds, (err, files) => {
// Check for installed stt models
fs.readdir(sttModels, (err, files) => {
for (let file of files) {
if (err) {
console.error(err);
}
for (const file of files) {
if (file.includes('.txt')) {
continue;
}
@ -120,35 +159,104 @@ fs.readdir(sttModels, (err, files) => {
sttModel.value = settings.STT.LANGUAGE;
});
// TODO: refactor obtaining audio devices.
async function getAudioDevices() {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
return;
}
const devices = await navigator.mediaDevices.enumerateDevices();
const audioOutputDevices = devices.filter((device) => device.kind === 'audiooutput');
const audioOutputDevices = devices.filter(device => device.kind === 'audiooutput');
audioOutputDevices.forEach((device) => {
const option = document.createElement('option');
option.text = device.label || `Output ${device.deviceId}`;
option.value = device.deviceId;
ttsAudioDevices.appendChild(option);
audioOutputDevices.forEach(device => {
const option1 = document.createElement('option');
const option2 = document.createElement('option');
option1.text = device.label || `Output ${device.deviceId}`;
option2.text = device.label || `Output ${device.deviceId}`;
option1.value = device.deviceId;
option2.value = device.deviceId;
ttsAudioDevices.appendChild(option1);
notificationSoundAudioDevices.appendChild(option2);
});
ttsAudioDevices.selectedIndex = settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE;
notificationSoundAudioDevices.selectedIndex = settings.AUDIO.SELECTED_NOTIFICATION_AUDIO_DEVICE;
}
getAudioDevices();
function setLanguagesinSelect(languageSelector, setting) {
let languageSelect = document.querySelector(languageSelector); // obtain the html reference of the google voices comboBox
function setSelectedLanguageinSelect(languageSelect, language) {
const button = languageSelect.querySelector('.SmallButton');
const languageElement = document.createElement('span');
languageElement.classList = `fi fi-${language.ISO3166} fis pop-selection`;
languageElement.setAttribute('tip', language.name);
button.innerHTML = '';
button.appendChild(languageElement);
addSingleTooltip(languageElement);
}
function setLanguagesinSelectx(languageSelector, language) {
const languageSelect = document.querySelector(languageSelector); // obtain the html reference of the google voices comboBox
const languageSelectContent = languageSelect.querySelector('.pop-content');
languageSelectContent.addEventListener('click', e => {
const parent = e.target.parentElement.id;
language = getLanguageProperties(e.target.getAttribute('value'));
if (parent === 'SEND_TRANSLATION_IN') {
settings.LANGUAGE.SEND_TRANSLATION_IN = language.IETF;
} else {
settings.LANGUAGE.SEND_TRANSLATION_OUT = language.IETF;
}
fs.writeFileSync(settingsPath, JSON.stringify(settings));
setSelectedLanguageinSelect(languageSelect, language);
});
for (const language in languageObject.languages) {
if (languageObject.languages.hasOwnProperty(language)) {
const iso639 = languageObject.languages[language]['ISO-639'];
if (Object.prototype.hasOwnProperty.call(languageObject.languages, language)) {
const IETF = languageObject.languages[language].IETF;
const ISO639 = languageObject.languages[language].ISO639;
const ISO3166 = languageObject.languages[language].ISO3166;
const option = document.createElement('div');
option.classList = 'language-select';
const languageElement = document.createElement('span');
languageElement.classList = `fi fi-${ISO3166} fis`;
languageElement.style.pointerEvents = 'none';
option.setAttribute('tip', language);
const text = document.createElement('span');
text.style.pointerEvents = 'none';
text.innerHTML = ` - ${ISO639}`;
option.setAttribute('value', IETF);
languageSelectContent.appendChild(option);
option.appendChild(languageElement);
option.appendChild(text);
addSingleTooltip(option);
}
}
setSelectedLanguageinSelect(languageSelect, language);
}
setLanguagesinSelectx('.pop.in', getLanguageProperties(settings.LANGUAGE.SEND_TRANSLATION_IN));
setLanguagesinSelectx('.pop.out', getLanguageProperties(settings.LANGUAGE.SEND_TRANSLATION_OUT));
function setLanguagesinSelect(languageSelector, setting) {
const languageSelect = document.querySelector(languageSelector); // obtain the html reference of the google voices comboBox
for (const language in languageObject.languages) {
if (Object.prototype.hasOwnProperty.call(languageObject.languages, language)) {
const IETF = languageObject.languages[language].IETF;
const ISO639 = languageObject.languages[language].ISO639;
const option = document.createElement('option');
option.value = iso639;
option.innerHTML = `${iso639} - ${language}`;
option.value = IETF;
option.innerHTML = `${ISO639} : ${language}`;
languageSelect.appendChild(option);
}
}
@ -156,13 +264,14 @@ function setLanguagesinSelect(languageSelector, setting) {
languageSelect.selectedIndex = setting;
}
setLanguagesinSelect('#language', settings.GENERAL.LANGUAGE);
setLanguagesinSelect('#language', settings.GENERAL.LANGUAGE_INDEX);
setLanguagesinSelect('#defaultLanguage', settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX);
setLanguagesinSelect('#secondaryLanguage', settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX);
setLanguagesinSelect('#TRANSLATE_TO', settings.LANGUAGE.TRANSLATE_TO_INDEX);
function addVoiceService(name) {
function addToselect(select) {
let ttsService = document.querySelector(select);
const ttsService = document.querySelector(select);
const option = document.createElement('option');
ttsService.appendChild(option);
@ -173,55 +282,97 @@ function addVoiceService(name) {
addToselect('#secondaryTTSService');
}
function determineTootlTipPosition(element) {
const horizontal = document.body.clientWidth / 2;
const vertical = document.body.clientHeight / 2;
element.tip.style.left = `${element.mouse.x}px`;
element.tip.style.top = `${element.mouse.y}px`;
const tipPosition = element.tip.getBoundingClientRect();
if (element.position.x < horizontal && element.position.y < vertical) {
element.tip.style.top = `${parseInt(element.tip.style.top) + 25}px`;
element.tip.style.left = `${parseInt(element.tip.style.left) + 10}px`;
}
if (element.position.x < horizontal && element.position.y > vertical) {
element.tip.style.top = `${parseInt(element.tip.style.top) - tipPosition.height}px`;
element.tip.style.left = `${parseInt(element.tip.style.left) + 10}px`;
}
if (element.position.x > horizontal && element.position.y < vertical) {
element.tip.style.top = `${parseInt(element.tip.style.top) + 25}px`;
element.tip.style.left = `${parseInt(element.tip.style.left) - tipPosition.width}px`;
}
if (element.position.x > horizontal && element.position.y > vertical) {
element.tip.style.top = `${parseInt(element.tip.style.top) - tipPosition.height}px`;
element.tip.style.left = `${parseInt(element.tip.style.left) - tipPosition.width}px`;
}
element.tip.style.visibility = 'visible';
}
// Small tooltip
Array.from(document.body.querySelectorAll('[tip]')).forEach((el) => {
function addSingleTooltip(el) {
const tip = document.createElement('div');
const body = document.querySelector('.container');
const element = el;
tip.classList.add('tooltip');
tip.classList.add('tooltiptext');
tip.innerText = el.getAttribute('tip');
tip.style.transform = `translate(${el.hasAttribute('tip-left') ? 'calc(-100% - 5px)' : '15px'}, ${
el.hasAttribute('tip-top') ? '-100%' : '15px'
})`;
if (el.src) {
const image = document.createElement('img');
image.src = el.src;
tip.appendChild(image);
}
body.appendChild(tip);
element.onmousemove = (e) => {
tip.style.left = `${e.x}px`;
tip.style.top = `${e.y}px`;
tip.style.zIndex = 1;
tip.style.visibility = 'visible';
tip.pointerEvents = 'none';
element.onmousemove = e => {
determineTootlTipPosition({
position: element.getBoundingClientRect(),
mouse: { x: e.x, y: e.y },
tip
});
};
element.onmouseleave = (e) => {
element.onmouseleave = e => {
tip.style.visibility = 'hidden';
};
}
Array.from(document.body.querySelectorAll('[tip]')).forEach(el => {
addSingleTooltip(el);
});
function showChatMessage(article, isUser) {
document.querySelector('#chatBox').appendChild(article);
let usernameHtml;
let msg;
let messages = Array.from(document.body.querySelectorAll('.msg-container'));
if (isUser) {
usernameHtml = article.querySelector('.username-user');
msg = article.querySelector('.msg-box-user');
} else {
usernameHtml = article.querySelector('.username');
msg = article.querySelector('.msg-box');
function showChatMessage(article) {
let body = null;
if (article !== undefined) {
body = document.getElementById('chatBox');
body.appendChild(article);
}
var style = getComputedStyle(usernameHtml);
var style2 = getComputedStyle(usernameHtml);
const messages = document.body.querySelectorAll('.msg-container');
const lastMessage = messages[messages.length - 1];
lastMessage.scrollIntoView({ behavior: 'smooth' });
lastMessage.scrollIntoView({ block: 'end', behavior: 'smooth' });
// const messageId = article.id;
// languageElement.setAttribute('id', article.id);
// console.log(article);
const username = article.querySelector('.username').innerHTML;
const image = article.querySelector('.user-img').src;
const statusCircle = article.querySelector('.status-circle').src;
const message = article.querySelector('.msg-box').innerHTML;
const postTime = article.querySelector('.post-time').innerHTML;
socket.emit('chat-out', { messageId, username, image, statusCircle, message, postTime });
}
function getPostTime() {
const date = new Date();
document.body.querySelectorAll('.container').innerHTML = date.getHours();
const hours = date.getHours();
var ampm = hours >= 12 ? 'PM' : 'AM';
const ampm = hours >= 12 ? 'PM' : 'AM';
const minutes = (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
const time = `${hours}:${minutes} ${ampm}`;
@ -250,6 +401,179 @@ function hideText(button, field) {
}
hideText('.password-toggle-btn1', '#TWITCH_OAUTH_TOKEN');
hideText('.password-toggle-btn2', '#TROVO_OAUTH_TOKEN');
hideText('.password-toggle-btn4', '#AMAZON_ACCESS_KEY');
hideText('.password-toggle-btn5', '#AMAZON_ACCESS_SECRET');
hideText('.password-toggle-btn6', '#GOOGLE_API_KEY');
function setZoomLevel(currentZoom, zoomIn) {
let newZoom = currentZoom.toFixed(2);
if (zoomIn === true && currentZoom < 4.95) {
newZoom = (currentZoom + 0.05).toFixed(2);
}
if (zoomIn === false && currentZoom > 0.25) {
newZoom = (currentZoom - 0.05).toFixed(2);
}
webFrame.setZoomFactor(parseFloat(newZoom));
settings.GENERAL.ZOOMLEVEL = newZoom;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
document.body.querySelector('#ZOOMLEVEL').value = (settings.GENERAL.ZOOMLEVEL * 100).toFixed(0);
}
function getLanguageProperties(languageToDetect) {
try {
const filteredLanguage = Object.keys(languageObject.languages).reduce(function (accumulator, currentValue) {
if (
languageObject.languages[currentValue].IETF === languageToDetect ||
languageObject.languages[currentValue].ISO639 === languageToDetect ||
languageObject.languages[currentValue].ISO3166 === languageToDetect
) {
accumulator[currentValue] = languageObject.languages[currentValue];
}
return accumulator;
}, {});
const language = {
name: Object.getOwnPropertyNames(filteredLanguage)[0],
ISO3166: filteredLanguage[Object.keys(filteredLanguage)[0]].ISO3166,
ISO639: filteredLanguage[Object.keys(filteredLanguage)[0]].ISO639,
IETF: filteredLanguage[Object.keys(filteredLanguage)[0]].IETF
};
return language;
} catch (e) {
// console.error(error);
return 'error';
}
}
let customEmotes = null;
if (fs.existsSync(customEmotesListSavePath)) {
const file = fs.readFileSync(customEmotesListSavePath);
customEmotes = JSON.parse(file);
customEmojiList = [...customEmojiList, ...customEmotes];
}
emojiPicker.customEmoji = customEmojiList;
function saveCustomEmotesToFile(emotes) {
const data = JSON.stringify(emotes);
customEmojiList = [...customEmojiList, data];
const savePath =
main.isPackaged === true ? path.join(resourcesPath, './custom-emotes.json') : path.join(resourcesPath, './config/custom-emotes.json');
fs.writeFile(savePath, data, error => {
if (error) {
console.error(error);
throw error;
}
});
}
function countWords(str) {
return str.trim().split(/\s+/).length;
}
// function saveSettingsToFile(settings) {
// const data = JSON.stringify(settings);
// const savePath =
// main.isPackaged === true ? path.join(resourcesPath, './settings.json') : path.join(resourcesPath, './config/settings.json');
// fs.writeFile(savePath, data, error => {
// if (error) {
// console.error(error);
// throw error;
// }
// });
// }
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// var videoElement = document.querySelector('video');
// var audioSelect = document.querySelector('select#audioSource');
// var videoSelect = document.querySelector('select#videoSource');
// audioSelect.onchange = getStream;
// videoSelect.onchange = getStream;
// getStream().then(getDevices).then(gotDevices);
// function getDevices() {
// // AFAICT in Safari this only gets default devices until gUM is called :/
// return navigator.mediaDevices.enumerateDevices();
// }
// function gotDevices(deviceInfos) {
// window.deviceInfos = deviceInfos; // make available to console
// console.log('Available input and output devices:', deviceInfos);
// for (const deviceInfo of deviceInfos) {
// const option = document.createElement('option');
// option.value = deviceInfo.deviceId;
// if (deviceInfo.kind === 'audioinput') {
// option.text = deviceInfo.label || `Microphone ${audioSelect.length + 1}`;
// audioSelect.appendChild(option);
// } else if (deviceInfo.kind === 'videoinput') {
// option.text = deviceInfo.label || `Camera ${videoSelect.length + 1}`;
// videoSelect.appendChild(option);
// }
// }
// }
// function getStream() {
// if (window.stream) {
// window.stream.getTracks().forEach(track => {
// track.stop();
// });
// }
// const audioSource = audioSelect.value;
// const videoSource = videoSelect.value;
// const constraints = {
// audio: { deviceId: audioSource ? { exact: audioSource } : undefined },
// video: { deviceId: videoSource ? { exact: videoSource } : undefined }
// };
// return navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError);
// }
// function gotStream(stream) {
// window.stream = stream; // make stream available to console
// audioSelect.selectedIndex = [...audioSelect.options].findIndex(option => option.text === stream.getAudioTracks()[0].label);
// videoSelect.selectedIndex = [...videoSelect.options].findIndex(option => option.text === stream.getVideoTracks()[0].label);
// videoElement.srcObject = stream;
// }
// function handleError(error) {
// console.error('Error: ', error);
// }
// const settingxs = {
// video: true
// };
// navigator.mediaDevices
// .getUserMedia(settingxs)
// .then(stream => {
// const video = document.getElementById('video');
// video.srcObject = stream;
// video.play();
// })
// .catch(err => {
// console.log(err);
// alert('Não há permissões para acessar a webcam.');
// });

View file

@ -2,16 +2,19 @@ function getBotResponse(input) {
// rock paper scissors
if (input === 'rock') {
return 'paper';
} if (input === 'paper') {
}
if (input === 'paper') {
return 'scissors';
} if (input === 'scissors') {
}
if (input === 'scissors') {
return 'rock';
}
// Simple responses
if (input === 'hello') {
return 'Hello there!';
} if (input === 'goodbye') {
}
if (input === 'goodbye') {
return 'Talk to you later!';
}
return 'Try asking something else!';

View file

@ -1,3 +1,5 @@
/* global settings */
const express = require('express');
const app = express();
const path = require('path');
@ -5,7 +7,7 @@ const http = require('http');
const localServer = http.createServer(app);
const io = require('socket.io')(localServer);
let requestCount = 0;
const requestCount = 0;
function startVtuberModule() {
if (!settings.MODULES.USE_VTUBER) {
@ -14,8 +16,8 @@ function startVtuberModule() {
app.use('/vtuber', express.static(path.join(__dirname, '../modules/vtuber/')));
let vtuber = document.body.querySelector('#BrowsersourceVtuber');
let vtuberframe = document.createElement('iframe');
const vtuber = document.body.querySelector('#BrowsersourceVtuber');
const vtuberframe = document.createElement('iframe');
vtuberframe.class = 'frame';
vtuberframe.src = `http://localhost:${settings.GENERAL.PORT}/vtuber`;
vtuberframe.style.width = '100%';
@ -26,15 +28,34 @@ function startVtuberModule() {
startVtuberModule();
function startChatBubbleModule() {
function startPNGtuberModule() {
if (!settings.MODULES.USE_PNGTUBER) {
return;
}
app.use('/pngtuber', express.static(path.join(__dirname, '../modules/pngtuber/')));
const pngtuber = document.body.querySelector('#BrowsersourcePNGTuber');
const pngtuberframe = document.createElement('iframe');
pngtuberframe.class = 'frame';
pngtuberframe.src = `http://localhost:${settings.GENERAL.PORT}/pngtuber`;
pngtuberframe.style.width = '100%';
pngtuberframe.style.height = '100%';
pngtuberframe.frameBorder = 0;
pngtuber.appendChild(pngtuberframe);
}
startPNGtuberModule();
function startChatWindowModule() {
if (!settings.MODULES.USE_CHATBUBBLE) {
return;
}
app.use('/chat', express.static(path.join(__dirname, '../modules/chat')));
let chat = document.body.querySelector('#BrowsersourceChat');
let chatframe = document.createElement('iframe');
const chat = document.body.querySelector('#BrowsersourceChatWindow');
const chatframe = document.createElement('iframe');
chatframe.class = 'frame';
chatframe.src = `http://localhost:${settings.GENERAL.PORT}/chat`;
chatframe.style.width = '100%';
@ -43,9 +64,26 @@ function startChatBubbleModule() {
chat.appendChild(chatframe);
}
startChatBubbleModule();
startChatWindowModule();
function startSTT() {}
function startChatBubbleModule() {
if (!settings.MODULES.USE_CHATBUBBLE) {
return;
}
app.use('/chatbubble', express.static(path.join(__dirname, '../modules/chatbubble')));
const chatBubble = document.body.querySelector('#BrowsersourceChatBubble');
const chatBubbleFrame = document.createElement('iframe');
chatBubbleFrame.class = 'frame';
chatBubbleFrame.src = `http://localhost:${settings.GENERAL.PORT}/chatbubble`;
chatBubbleFrame.style.width = '100%';
chatBubbleFrame.style.height = '100%';
chatBubbleFrame.frameBorder = 0;
chatBubble.appendChild(chatBubbleFrame);
}
startChatBubbleModule();
// Middleware to conditionally serve routes
app.use((req, res, next) => {
@ -60,16 +98,17 @@ app.use((req, res, next) => {
localServer.listen(settings.GENERAL.PORT, () => {
startVtuberModule();
startChatWindowModule();
startChatBubbleModule();
if (settings.TTS.USE_TTS) {
}
});
// Handle socket connections
io.on('connection', (socket) => {
io.on('connection', socket => {
// Receive data from the client
socket.on('message', (data) => {});
socket.on('message', data => {});
socket.on('chat-out', message => {
socket.broadcast.emit('chat-in', message);
});
// Receive data from the client
socket.on('xxx', (logoUrl, username, message) => {
@ -79,4 +118,4 @@ io.on('connection', (socket) => {
socket.on('disconnect', () => {});
});
module.exports = { startVtuberModule, startChatBubbleModule };
module.exports = { startVtuberModule, startChatBubbleModule, startChatWindowModule };

View file

@ -1,7 +1,10 @@
/* global settings,main, betterTTVAutocomplete sttModels, trovo, setZoomLevel, webFrame, theme, fs, settingsPath, ini, startVoiceRecognition,notificationSoundAudioDevices, ttsAudioDevices, notificationSound, path, resourcesPath, ipcRenderer, auth, shell, sound, twitch, server, backend */
function getGeneralSettings() {
// General
document.body.querySelector('#PORT').value = settings.GENERAL.PORT;
document.body.querySelector('#ZOOMLEVEL').value = settings.GENERAL.ZOOMLEVEL * 100;
webFrame.setZoomFactor(parseFloat(settings.GENERAL.ZOOMLEVEL));
// Theme
document.querySelector('#USE_CUSTOM_THEME').value = settings.THEME.USE_CUSTOM_THEME;
document.body.querySelector('#USE_CUSTOM_THEME').checked = settings.THEME.USE_CUSTOM_THEME === true ? 1 : 0;
@ -12,6 +15,9 @@ function getGeneralSettings() {
// Language detection
document.body.querySelector('#USE_DETECTION').checked = settings.LANGUAGE.USE_DETECTION;
document.body.querySelector('#OUTPUT_TO_TTS').checked = settings.LANGUAGE.OUTPUT_TO_TTS;
document.body.querySelector('#SEND_TRANSLATION').checked = settings.LANGUAGE.SEND_TRANSLATION;
document.body.querySelector('#BROADCAST_TRANSLATION').checked = settings.LANGUAGE.BROADCAST_TRANSLATION;
// TTS
document.body.querySelector('#USE_TTS').checked = settings.TTS.USE_TTS;
@ -24,14 +30,26 @@ function getGeneralSettings() {
document.body.querySelector('#TWITCH_CHANNEL_NAME').value = settings.TWITCH.CHANNEL_NAME;
document.body.querySelector('#TWITCH_OAUTH_TOKEN').value = settings.TWITCH.OAUTH_TOKEN;
// Trovo
document.body.querySelector('#USE_TROVO').checked = settings.TROVO.USE_TWITCH;
document.body.querySelector('#TROVO_CHANNEL_NAME').value = settings.TROVO.CHANNEL_NAME;
document.body.querySelector('#TROVO_OAUTH_TOKEN').value = settings.TROVO.OAUTH_TOKEN;
// Modules
document.body.querySelector('#USE_MODULES').checked = settings.MODULES.USE_MODULES;
document.body.querySelector('#USE_VTUBER').checked = settings.MODULES.USE_VTUBER;
document.body.querySelector('#VTUBER_URL').value = `http://localhost:${settings.GENERAL.PORT}/vtuber/`;
document.body.querySelector('#VTUBER_URL').value = `http://localhost:${settings.GENERAL.PORT}/vtuber`;
showMenuButton('#btnBrowsersourceVtuber', settings.MODULES.USE_VTUBER);
document.body.querySelector('#USE_PNGTUBER').checked = settings.MODULES.USE_PNGTUBER;
document.body.querySelector('#PNGTUBER_URL').value = `http://localhost:${settings.GENERAL.PORT}/pngtuber`;
showMenuButton('#btnBrowsersourcePNGTuber', settings.MODULES.USE_PNGTUBER);
document.body.querySelector('#USE_CHATBUBBLE').checked = settings.MODULES.USE_CHATBUBBLE;
document.body.querySelector('#CHATBUBBLE_URL').value = `http://localhost:${settings.GENERAL.PORT}/chat/`;
showMenuButton('#btnBrowsersourceChat', settings.GENERAL.USE_CHATBUBBLE);
document.body.querySelector('#CHATBUBBLE_URL').value = `http://localhost:${settings.GENERAL.PORT}/chatbubble`;
document.body.querySelector('#USE_CHATWINDOW').checked = settings.MODULES.USE_CHATWINDOW;
document.body.querySelector('#CHATWINDOW_URL').value = `http://localhost:${settings.GENERAL.PORT}/chat`;
document.body.querySelector('#USE_FACEMASK').checked = settings.MODULES.USE_FACEMASK;
showMenuButton('#btnBrowsersourceChatBubble', settings.GENERAL.USE_CHATWINDOW);
showMenuButton('#btnFaceMask', settings.MODULES.USE_FACEMASK);
// Amazon
document.body.querySelector('#USE_AMAZON').checked = settings.AMAZON.USE_AMAZON;
@ -44,137 +62,208 @@ function getGeneralSettings() {
}
document.body.querySelector('#primaryAmazonVoice').addEventListener('change', () => {
var select = document.querySelector('#primaryAmazonVoice');
const select = document.querySelector('#primaryAmazonVoice');
settings.AMAZON.PRIMARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Amazon primary voice!', 'success');
});
document.body.querySelector('#secondaryAmazonVoice').addEventListener('change', () => {
var select = document.querySelector('#secondaryAmazonVoice');
const select = document.querySelector('#secondaryAmazonVoice');
settings.AMAZON.SECONDARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Amazon secondary voice!', 'success');
});
document.body.querySelector('#primaryGoogleVoice').addEventListener('change', () => {
var select = document.querySelector('#primaryGoogleVoice');
const select = document.querySelector('#primaryGoogleVoice');
settings.GOOGLE.PRIMARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Google primary voice!', 'success');
});
document.body.querySelector('#secondaryGoogleVoice').addEventListener('change', () => {
var select = document.querySelector('#secondaryGoogleVoice');
const select = document.querySelector('#secondaryGoogleVoice');
settings.GOOGLE.SECONDARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Google secondary voice!', 'success');
});
document.body.querySelector('#primaryVoice').addEventListener('change', () => {
var select = document.querySelector('#primaryVoice');
const select = document.querySelector('#primaryVoice');
settings.TTS.PRIMARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved primary voice!', 'success');
});
document.body.querySelector('#microphone').addEventListener('change', () => {
var select = document.querySelector('#microphone');
const select = document.querySelector('#microphone');
settings.STT.MICROPHONE = select.value;
settings.STT.MICROPHONE_ID = select.options[select.selectedIndex].text;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved microphone!', 'success');
startVoiceRecognition();
});
document.body.querySelector('#sttModel').addEventListener('change', () => {
var select = document.querySelector('#sttModel');
const select = document.querySelector('#sttModel');
settings.STT.LANGUAGE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved voice detection language!', 'success');
});
document.body.querySelector('#defaultLanguage').addEventListener('change', () => {
var select = document.querySelector('#defaultLanguage');
const select = document.querySelector('#defaultLanguage');
settings.TTS.PRIMARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
settings.TTS.PRIMARY_TTS_LANGUAGE = select.options[select.selectedIndex].text;
fs.writeFileSync(settingsPath, ini.stringify(settings));
settings.TTS.PRIMARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved default language!', 'success');
});
document.body.querySelector('#secondaryVoice').addEventListener('change', () => {
var select = document.querySelector('#secondaryVoice');
const select = document.querySelector('#secondaryVoice');
settings.TTS.SECONDARY_VOICE = select.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved secondary voice!', 'success');
});
document.body.querySelector('#secondaryLanguage').addEventListener('change', () => {
var select = document.querySelector('#secondaryLanguage');
const select = document.querySelector('#secondaryLanguage');
settings.TTS.SECONDARY_TTS_LANGUAGE_INDEX = select.selectedIndex;
settings.TTS.SECONDARY_TTS_LANGUAGE = select.options[select.selectedIndex].text;
fs.writeFileSync(settingsPath, ini.stringify(settings));
settings.TTS.SECONDARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved secondary language!', 'success');
});
document.body.querySelector('#language').addEventListener('change', () => {
const select = document.querySelector('#language');
settings.GENERAL.LANGUAGE_INDEX = select.selectedIndex;
settings.GENERAL.LANGUAGE = select.options[select.selectedIndex].value;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved language!', 'success');
});
function setTranslateToOptions() {
const options = document.querySelectorAll('.TRANSLATE_TO');
const index = parseInt(settings.LANGUAGE.TRANSLATE_TO_INDEX);
if (index === 0) {
settings.LANGUAGE.BROADCAST_TRANSLATION = false;
settings.LANGUAGE.OUTPUT_TO_TTS = false;
options.forEach(item => {
item.style.visibility = 'hidden';
item.style.height = '0px';
item.checked = false;
});
} else {
options.forEach(item => {
item.style.visibility = '';
item.style.height = '';
});
}
}
setTranslateToOptions();
document.body.querySelector('#TRANSLATE_TO').addEventListener('change', () => {
const select = document.querySelector('#TRANSLATE_TO');
settings.LANGUAGE.TRANSLATE_TO_INDEX = select.selectedIndex;
settings.LANGUAGE.TRANSLATE_TO = select.options[select.selectedIndex].value;
setTranslateToOptions();
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved primary voice!', 'success');
});
document.body.querySelector('#ttsAudioDevice').addEventListener('change', () => {
settings.AUDIO.TTS_AUDIO_DEVICE = ttsAudioDevices.value;
settings.AUDIO.SELECTED_TTS_AUDIO_DEVICE = ttsAudioDevices.selectedIndex;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved audio device!', 'success');
});
document.body.querySelector('#notificationSoundAudioDevice').addEventListener('change', () => {
settings.AUDIO.SELECTED_NOTIFICATION_AUDIO_DEVICE = notificationSoundAudioDevices.value;
settings.AUDIO.NOTIFICATION_AUDIO_DEVICE = notificationSoundAudioDevices.selectedIndex;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved audio device!', 'success');
});
document.body.querySelector('#TWITCH_CHANNEL_NAME').addEventListener('change', () => {
settings.TWITCH.CHANNEL_NAME = document.body.querySelector('#TWITCH_CHANNEL_NAME').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
let button = document.body.querySelector('#TestTwitchCredentials');
button.className = 'AdvancedMenuButton';
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Channel name, please restart the application to reset twitch service', 'warning');
if (settings.TWITCH.CHANNEL_NAME !== '') {
twitch.getTwitchChannelId(settings.TWITCH.CHANNEL_NAME).then(data => {
settings.TWITCH.CHANNEL_USER_ID = data;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Obtained channel info succesfully', 'success');
twitch.getUserAvailableTwitchEmotes();
});
}
});
document.body.querySelector('#TROVO_CHANNEL_NAME').addEventListener('change', () => {
settings.TROVO.CHANNEL_NAME = document.body.querySelector('#TROVO_CHANNEL_NAME').value;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Channel name, please restart the application to reset twitch service', 'warning');
if (settings.TROVO.CHANNEL_NAME !== '') {
trovo.getTrovoChannelId();
}
});
document.body.querySelector('#TWITCH_OAUTH_TOKEN').addEventListener('change', () => {
settings.TWITCH.OAUTH_TOKEN = document.body.querySelector('#TWITCH_OAUTH_TOKEN').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
createNotification('Saved OAuth token!', 'success');
let button = document.body.querySelector('#TestTwitchCredentials');
button.className = 'AdvancedMenuButton';
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved OAuth token, please restart the application to reset twitch service', 'warning');
});
document.body.querySelector('#TROVO_OAUTH_TOKEN').addEventListener('change', () => {
settings.TROVO.OAUTH_TOKEN = document.body.querySelector('#TROVO_OAUTH_TOKEN').value;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved OAuth token, please restart the application to reset twitch service', 'warning');
});
setInputFilter(
document.body.querySelector('#PORT'),
function (value) {
return /^\d*\.?\d*$/.test(value); // Allow digits and '.' only, using a RegExp.
},
"Only digits and '.' are allowed"
);
document.body.querySelector('#PORT').addEventListener('change', () => {
settings.GENERAL.PORT = document.body.querySelector('#PORT').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved port, please restart the application to reset the port', 'warning');
});
document.body.querySelector('#AMAZON_ACCESS_KEY').addEventListener('change', () => {
settings.AMAZON.ACCESS_KEY = document.body.querySelector('#AMAZON_ACCESS_KEY').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Amazon access key!', 'success');
});
document.body.querySelector('#AMAZON_ACCESS_SECRET').addEventListener('change', () => {
settings.AMAZON.ACCESS_SECRET = document.body.querySelector('#AMAZON_ACCESS_SECRET').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Amazon access secret!', 'success');
});
document.body.querySelector('#GOOGLE_API_KEY').addEventListener('change', () => {
settings.GOOGLE.API_KEY = document.body.querySelector('#GOOGLE_API_KEY').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved Google api key!', 'success');
});
document.body.querySelector('#notification').addEventListener('change', () => {
settings.AUDIO.NOTIFICATION_SOUND = notificationSound.selectedIndex;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification('Saved notification sound!', 'success');
});
function showMenuButton(menuButton, toggle) {
let option = document.body.querySelector(menuButton);
const option = document.body.querySelector(menuButton);
if (!toggle) {
option.style.display = 'none';
} else {
@ -196,10 +285,15 @@ function createNotification(message = null, type = null) {
alertSound = 'error.mp3';
}
let notfication = new Audio(path.join(resourcesPath, `./sounds/notifications/${alertSound}`));
if (settings.AUDIO.USE_NOTIFICATION_SOUNDS) {
const notfication = new Audio(
path.join(resourcesPath, main.isPackaged ? `./sounds/notifications/${alertSound}` : `../sounds/notifications/${alertSound}`)
);
notfication.volume = settings.AUDIO.NOTIFICATION_VOLUME / 100;
notfication.play();
setTimeout(() => notification.remove(), 10000);
}
setTimeout(() => notification.remove(), 3000);
}
// Check for configs
@ -231,6 +325,14 @@ function toggleRadio(toggle, inputs) {
}
}
document.body.querySelector('#OPEN_SETTINGS_FILE').addEventListener('click', () => {
shell.openExternal(settingsPath);
});
document.body.querySelector('#Info_VOICE_MODELS_FOLDER').addEventListener('click', () => {
shell.openExternal(sttModels);
});
// #region Use Custom theme toggle logic
document.body.querySelector('#USE_CUSTOM_THEME').addEventListener('click', () => {
const toggle = document.getElementById('USE_CUSTOM_THEME').checked;
@ -238,7 +340,7 @@ document.body.querySelector('#USE_CUSTOM_THEME').addEventListener('click', () =>
toggleRadio(toggle, inputs);
settings.THEME.USE_CUSTOM_THEME = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
theme.setTheme();
createNotification(`${toggle ? 'Enabled' : 'Disabled'} custom theme!`, 'success');
});
@ -249,15 +351,32 @@ document.body.querySelector('#min-button').addEventListener('click', () => {
});
// #region Top bar buttons
document.body.querySelector('#Info_USERNAME').addEventListener('click', async () => {
let element = document.body.querySelector('#TWITCH_OAUTH_TOKEN');
document.body.querySelector('#Info_TWITCH_USERNAME').addEventListener('click', async () => {
const element = document.body.querySelector('#TWITCH_OAUTH_TOKEN');
element.value = await auth.getTwitchOauthToken();
twitch.checkIfTokenIsValid();
createNotification('Saved OAuth token!', 'success');
});
let hideInputToggleButton = document.body.querySelectorAll('.password-toggle-btn .password-toggle-icon .fa-eye-slash');
hideInputToggleButton.forEach((item) => {
document.body.querySelector('#Info_TROVO_USERNAME').addEventListener('click', async () => {
const element = document.body.querySelector('#TROVO_OAUTH_TOKEN');
element.value = await auth.getTrovoOAuthToken();
console.log(element.value);
// trovo.checkIfTokenIsValid();
createNotification('Saved OAuth token!', 'success');
});
document.body.querySelector('#GetBetterTtvEmotes').addEventListener('click', async () => {
twitch.getBetterTtvGLobalEmotes();
if (betterTTVAutocomplete.tokens.length !== 0) {
twitch.getBetterTtvChannelsEmotes();
}
createNotification('Saved BetterTTV emotes!', 'success');
});
const hideInputToggleButton = document.body.querySelectorAll('.password-toggle-btn .password-toggle-icon .fa-eye-slash');
hideInputToggleButton.forEach(item => {
item.addEventListener('click', () => {
if (item.classList.contains('fa-eye')) {
item.classList.remove('fa-eye');
@ -280,7 +399,7 @@ function hideOrShowViewerPanel() {
menu.classList.remove('collapse-menu-right');
leftCircle.classList.remove('collapse-circle-right');
}
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
}
hideOrShowViewerPanel();
@ -295,11 +414,19 @@ document.body.querySelector('#VIEWERS_PANEL').addEventListener('click', () => {
});
document.body.querySelector('#Info_VTUBER').addEventListener('click', () => {
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/vtuber/`);
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/vtuber`);
});
document.body.querySelector('#Info_PNGTUBER').addEventListener('click', () => {
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/pngtuber`);
});
document.body.querySelector('#Info_CHATBUBBLE').addEventListener('click', () => {
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/chat/`);
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/chatbubble`);
});
document.body.querySelector('#Info_CHATWINDOW').addEventListener('click', () => {
shell.openExternal(`http://localhost:${settings.GENERAL.PORT}/chat`);
});
document.body.querySelector('#max-button').addEventListener('click', () => {
@ -333,7 +460,7 @@ toggleTwitch();
document.body.querySelector('#USE_TWITCH').addEventListener('click', () => {
const toggle = document.getElementById('USE_TWITCH').checked;
settings.TWITCH.USE_TWITCH = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputTwitch');
toggleRadio(toggle, inputs);
twitch = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './twitch')) : null;
@ -351,7 +478,7 @@ toggleGoogle();
document.body.querySelector('#USE_GOOGLE').addEventListener('click', () => {
const toggle = document.getElementById('USE_GOOGLE').checked;
settings.GOOGLE.USE_GOOGLE = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputGoogle');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Google settings!`, 'success');
@ -368,7 +495,7 @@ toggleAmazon();
document.body.querySelector('#USE_AMAZON').addEventListener('click', () => {
const toggle = document.getElementById('USE_AMAZON').checked;
settings.AMAZON.USE_AMAZON = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputAmazon');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Amazon settings!`, 'success');
@ -385,34 +512,68 @@ toggleServer();
document.body.querySelector('#USE_MODULES').addEventListener('click', () => {
const toggle = document.getElementById('USE_MODULES').checked;
settings.MODULES.USE_MODULES = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputServer');
toggleRadio(toggle, inputs);
createNotification(
`${toggle ? 'Enabled' : 'Disabled'} server settings!, the service will stop working after restarting the application
${toggle ? '' : ', the service will stop working after restarting the application'}`,
'success',
'success'
);
});
document.body.querySelector('#USE_VTUBER').addEventListener('change', () => {
const toggle = document.getElementById('USE_VTUBER').checked;
settings.MODULES.USE_VTUBER = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
showMenuButton('#btnBrowsersourceVtuber', toggle);
createNotification(
`${toggle ? 'Enabled' : 'Disabled'} Vtuber setting!
${toggle ? '' : ', the service will stop working after restarting the application'}`,
'success',
'success'
);
server.startVtuberModule();
});
document.body.querySelector('#USE_PNGTUBER').addEventListener('change', () => {
const toggle = document.getElementById('USE_PNGTUBER').checked;
settings.MODULES.USE_PNGTUBER = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
showMenuButton('#btnBrowsersourcePNGTuber', toggle);
createNotification(
`${toggle ? 'Enabled' : 'Disabled'} PNGtuber setting!
${toggle ? '' : ', the service will stop working after restarting the application'}`,
'success'
);
server.startVtuberModule();
});
document.body.querySelector('#USE_FACEMASK').addEventListener('change', () => {
const toggle = document.getElementById('USE_FACEMASK').checked;
settings.MODULES.USE_FACEMASK = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
showMenuButton('#btnFaceMask', toggle);
createNotification(
`${toggle ? 'Enabled' : 'Disabled'} FaceMask setting!
${toggle ? '' : ', the service will stop working after restarting the application'}`,
'success'
);
// server.startVtuberModule();
});
document.body.querySelector('#USE_CHATWINDOW').addEventListener('change', () => {
const toggle = document.getElementById('USE_CHATWINDOW').checked;
settings.MODULES.USE_CHATWINDOW = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification(`${toggle ? 'Enabled' : 'Disabled'} chat window setting!`, 'success');
server.startChatWindowModule();
});
document.body.querySelector('#USE_CHATBUBBLE').addEventListener('change', () => {
const toggle = document.getElementById('USE_CHATBUBBLE').checked;
settings.MODULES.USE_CHATBUBBLE = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
showMenuButton('#btnBrowsersourceChat', toggle);
fs.writeFileSync(settingsPath, JSON.stringify(settings));
showMenuButton('#btnBrowsersourceChatBubble', toggle);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} chatbubble setting!`, 'success');
server.startChatBubbleModule();
});
@ -428,7 +589,7 @@ toggleTTS();
document.body.querySelector('#USE_TTS').addEventListener('change', () => {
const toggle = document.getElementById('USE_TTS').checked;
settings.TTS.USE_TTS = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputTTS');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} text to speech!`, 'success');
@ -445,12 +606,49 @@ toggleSTT();
document.body.querySelector('#USE_STT').addEventListener('change', () => {
const toggle = document.getElementById('USE_STT').checked;
settings.STT.USE_STT = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputSTT');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} speech to text!`, 'success');
});
function toggleSendTranslation() {
const toggle = settings.LANGUAGE.SEND_TRANSLATION;
const inputs = document.getElementsByClassName('send-translation');
toggleRadio(toggle, inputs);
}
toggleSendTranslation();
document.body.querySelector('#SEND_TRANSLATION').addEventListener('change', () => {
const toggle = document.getElementById('SEND_TRANSLATION').checked;
settings.LANGUAGE.SEND_TRANSLATION = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('send-translation');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Sending translations!`, 'success');
});
document.body.querySelector('#OUTPUT_TO_TTS').addEventListener('change', () => {
let toggle = document.getElementById('OUTPUT_TO_TTS').checked;
if (!settings.TTS.USE_TTS) {
toggle = false;
createNotification('Enable TTS first', 'error');
return;
}
settings.LANGUAGE.OUTPUT_TO_TTS = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Outputting translations to TTS!`, 'success');
});
document.body.querySelector('#BROADCAST_TRANSLATION').addEventListener('change', () => {
const toggle = document.getElementById('BROADCAST_TRANSLATION').checked;
settings.LANGUAGE.BROADCAST_TRANSLATION = toggle;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Language detection!`, 'success');
});
function toggleLanguageDetection() {
const toggle = settings.LANGUAGE.USE_DETECTION;
const inputs = document.getElementsByClassName('languageDetectionInput');
@ -462,7 +660,15 @@ toggleLanguageDetection();
document.body.querySelector('#USE_DETECTION').addEventListener('change', () => {
const toggle = document.getElementById('USE_DETECTION').checked;
settings.LANGUAGE.USE_DETECTION = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
if (!toggle) {
settings.LANGUAGE.BROADCAST_TRANSLATION = false;
document.body.querySelector('#BROADCAST_TRANSLATION').checked = false;
settings.LANGUAGE.OUTPUT_TO_TTS = false;
document.body.querySelector('#OUTPUT_TO_TTS').checked = false;
}
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('languageDetectionInput');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Language detection!`, 'success');
@ -479,16 +685,16 @@ toggleNotificationSounds();
document.body.querySelector('#USE_NOTIFICATION_SOUNDS').addEventListener('change', () => {
const toggle = document.getElementById('USE_NOTIFICATION_SOUNDS').checked;
settings.AUDIO.USE_NOTIFICATION_SOUNDS = toggle;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const inputs = document.getElementsByClassName('inputNotificationSound');
toggleRadio(toggle, inputs);
createNotification(`${toggle ? 'Enabled' : 'Disabled'} notification sounds!`, 'success');
});
document.body.querySelector('#notificationVolume').addEventListener('change', () => {
let element = document.body.querySelector('#notificationVolume');
const element = document.body.querySelector('#notificationVolume');
settings.AUDIO.NOTIFICATION_VOLUME = element.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const slider = document.querySelector('#notificationVolumeSlider');
slider.value = settings.AUDIO.NOTIFICATION_VOLUME;
@ -508,7 +714,7 @@ document.body.querySelector('#notificationVolumeSlider').addEventListener('chang
e.style.setProperty('--tiempotemporal', e.value);
document.querySelector('#notificationVolume').value = e.value;
settings.AUDIO.NOTIFICATION_VOLUME = e.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
});
});
@ -524,9 +730,9 @@ if (settings.AUDIO.NOTIFICATION_VOLUME) {
}
document.body.querySelector('#ttsVolume').addEventListener('change', () => {
let element = document.body.querySelector('#ttsVolume');
const element = document.body.querySelector('#ttsVolume');
settings.AUDIO.TTS_VOLUME = element.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const slider = document.querySelector('#ttsVolumeSlider');
slider.value = settings.AUDIO.TTS_VOLUME;
@ -546,7 +752,7 @@ document.body.querySelector('#ttsVolumeSlider').addEventListener('change', () =>
e.style.setProperty('--tiempotemporal', e.value);
document.querySelector('#ttsVolume').value = e.value;
settings.AUDIO.TTS_VOLUME = e.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
});
});
@ -562,9 +768,9 @@ if (settings.AUDIO.TTS_VOLUME) {
}
document.body.querySelector('#ttsVolume').addEventListener('change', () => {
let element = document.body.querySelector('#ttsVolume');
const element = document.body.querySelector('#ttsVolume');
settings.AUDIO.TTS_VOLUME = element.value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
const slider = document.querySelector('#ttsVolumeSlider');
slider.value = settings.AUDIO.TTS_VOLUME;
@ -572,29 +778,102 @@ document.body.querySelector('#ttsVolume').addEventListener('change', () => {
});
document.body.querySelector('#TestDefaultTTSButton').addEventListener('click', async () => {
if (!settings.TTS.PRIMARY_VOICE) {
return;
}
const text = document.getElementById('testPrimaryTTS').value;
const requestData = {
message: `user: ${text}`,
voice: settings.TTS.PRIMARY_VOICE,
voice: settings.TTS.PRIMARY_VOICE
};
let count = await backend.getInternalTTSAudio(requestData);
let textObject = { filtered: text, formatted: text };
const count = await backend.getInternalTTSAudio(requestData);
const textObject = { filtered: text, formatted: text };
sound.playAudio({ service: 'Internal', message: textObject, count });
});
document.body.querySelector('#TestSecondaryTTSButton').addEventListener('click', async () => {
if (!settings.TTS.SECONDARY_VOICE) {
return;
}
const text = document.getElementById('testSecondaryTTS').value;
const requestData = {
message: `user: ${text}`,
voice: settings.TTS.SECONDARY_VOICE,
voice: settings.TTS.SECONDARY_VOICE
};
let count = await backend.getInternalTTSAudio(requestData);
let textObject = { filtered: text, formatted: text };
const count = await backend.getInternalTTSAudio(requestData);
const textObject = { filtered: text, formatted: text };
sound.playAudio({ service: 'Internal', message: textObject, count });
});
// Restricts input for the given textbox to the given inputFilter function.
function setInputFilter(textbox, inputFilter, errMsg) {
['input', 'keydown', 'keyup', 'mousedown', 'mouseup', 'select', 'contextmenu', 'drop', 'focusout'].forEach(function (event) {
textbox.addEventListener(event, function (e) {
if (inputFilter(this.value)) {
// Accepted value.
if (['keydown', 'mousedown', 'focusout'].indexOf(e.type) >= 0) {
this.classList.remove('input-error');
this.setCustomValidity('');
}
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
} else if (Object.prototype.hasOwnProperty.call(this, 'oldValue')) {
// Rejected value: restore the previous one.
this.classList.add('input-error');
this.setCustomValidity(errMsg);
this.reportValidity();
this.value = this.oldValue;
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
} else {
// Rejected value: nothing to restore.
this.value = '';
}
});
});
}
webFrame.setVisualZoomLevelLimits(1, 5);
document.body.addEventListener('wheel', e => {
if (e.ctrlKey) {
const currentZoom = webFrame.getZoomFactor();
const zoomIn = Boolean(e.deltaY < 0);
setZoomLevel(currentZoom, zoomIn);
}
});
setInputFilter(
document.body.querySelector('#ZOOMLEVEL'),
function (value) {
return /^\d*\.?\d*$/.test(value); // Allow digits and '.' only, using a RegExp.
},
"Only digits and '.' are allowed"
);
document.body.querySelector('#ZOOMLEVEL').addEventListener('change', () => {
const newZoom = parseInt(document.body.querySelector('#ZOOMLEVEL').value) / 100;
settings.GENERAL.ZOOMLEVEL = newZoom;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
setZoomLevel(newZoom, null);
createNotification('Saved zoom new level', 'warning');
});
document.body.querySelector('emoji-picker').addEventListener('emoji-click', e => {
// console.log(e.detail);
const div = document.getElementById('textInput');
if (e.detail.unicode === undefined) {
div.value += e.detail.name + ' ';
} else {
div.value += e.detail.unicode + ' ';
}
div.focus();
});
module.exports = {
getGeneralSettings,
createNotification
};

View file

@ -1,23 +1,23 @@
let trueMessage = '';
let currentLogoUrl = '';
let currentUsername = '';
let voiceSoundArray = [];
let status = 0;
let counter = 0;
/* global ttsAudioFile, main, path, getLanguageProperties, resourcesPath, settings, fs, notificationSound, backend, socket, requestData */
const playTTS = (data) =>
new Promise((resolve) => {
ttsAudioFile = path.join(resourcesPath, `./sounds/tts/${data.service}_${data.count}.mp3`);
const voiceSoundArray = [];
let status = 0;
const counter = 0;
const playTTS = data =>
new Promise(resolve => {
ttsAudioFile = path.join(
resourcesPath,
main.isPackaged ? `./sounds/${data.service}_${data.count}.mp3` : `../sounds/${data.service}_${data.count}.mp3`
);
const tts = new Audio(ttsAudioFile);
console.log(settings.AUDIO.TTS_AUDIO_DEVICE);
tts.setSinkId(settings.AUDIO.TTS_AUDIO_DEVICE);
tts.addEventListener('ended', () => {
console.log('ended');
fs.unlink(ttsAudioFile, (err) => {
// console.log('ended');
fs.unlink(ttsAudioFile, err => {
if (err) {
console.error('TEST');
console.error(err);
resolve('finished');
return;
}
@ -25,15 +25,19 @@ const playTTS = (data) =>
});
});
tts.setSinkId(settings.AUDIO.TTS_AUDIO_DEVICE)
tts
.setSinkId(settings.AUDIO.TTS_AUDIO_DEVICE)
.then(() => {
console.log('playing');
// console.log('playing');
tts.volume = settings.AUDIO.TTS_VOLUME / 100;
tts.play().catch((error) => {
tts.play().catch(error => {
if (error) {
console.error(error);
}
resolve('finished');
});
})
.catch((error) => {
.catch(error => {
console.error('Failed to set audio output device:', error);
resolve('finished');
});
@ -56,11 +60,29 @@ function add(data) {
function playNotificationSound() {
if (settings.AUDIO.USE_NOTIFICATION_SOUNDS) {
let notfication = new Audio(
path.join(resourcesPath, `./sounds/notifications/${notificationSound.options[settings.AUDIO.NOTIFICATION_SOUND].text}`),
const notfication = new Audio(
path.join(
resourcesPath,
main.isPackaged
? `./sounds/notifications/${notificationSound.options[settings.AUDIO.NOTIFICATION_SOUND].text}`
: `../sounds/notifications/${notificationSound.options[settings.AUDIO.NOTIFICATION_SOUND].text}`
)
);
notfication
.setSinkId(settings.AUDIO.SELECTED_NOTIFICATION_AUDIO_DEVICE)
.then(() => {
// console.log('playing');
notfication.volume = settings.AUDIO.NOTIFICATION_VOLUME / 100;
notfication.play();
notfication.play().catch(error => {
if (error) {
console.error(error);
}
});
})
.catch(error => {
console.error('Failed to set audio output device:', error);
});
}
}
@ -71,37 +93,40 @@ function playAudio(data) {
}
}
async function playVoice(filteredMessage, logoUrl, username, message) {
trueMessage = filteredMessage;
currentLogoUrl = logoUrl;
currentUsername = username;
let textObject = { filtered: filteredMessage, formatted: message };
let voice;
textObject.filtered = `${username}: ${filteredMessage}`;
async function playVoice(message) {
if (!settings.TTS.PRIMARY_VOICE) {
return;
}
const textObject = { filtered: message.filteredMessage, formatted: message.formattedMessage };
let voice = settings.TTS.PRIMARY_VOICE;
textObject.filtered = `${message.username}: ${message.filteredMessage}`;
// if (
// settings.TTS.PRIMARY_TTS_LANGUAGE.toLowerCase() !== settings.TTS.SECONDARY_TTS_LANGUAGE.toLowerCase() &&
// language[0].lang === settings.TTS.SECONDARY_TTS_LANGUAGE.toLowerCase()
// ) {
// voice = settings.TTS.SECONDARY_TTS_NAME;
// textObject.filtered = `${username}: ${filteredMessage}`;
// } else {
// voice = settings.TTS.PRIMARY_TTS_NAME;
// textObject.filtered = `${username}: ${filteredMessage}`;
// }
if (
settings.LANGUAGE.USE_DETECTION &&
settings.TTS.SECONDARY_VOICE &&
message.filteredMessage.length > 20 &&
countWords(message.filteredMessage) > 4
) {
const secondaryTTSLanguage = getLanguageProperties(settings.TTS.SECONDARY_TTS_LANGUAGE);
if (message.language.detectedLanguage === null || message.language.detectedLanguage.ISO639 === secondaryTTSLanguage.ISO639) {
voice = settings.TTS.SECONDARY_VOICE;
textObject.filtered = message.originalMessage ? message.originalMessage : message.filteredMessage;
}
}
const service = document.getElementById('primaryTTSService').value;
switch (service) {
case 'Internal':
case 'Internal': {
const requestData = {
message: textObject.filtered,
voice: settings.TTS.PRIMARY_VOICE,
voice: voice
};
let count = await backend.getInternalTTSAudio(requestData);
const count = await backend.getInternalTTSAudio(requestData);
playAudio({ service, message: textObject, count });
break;
}
case 'Amazon':
// playAudio({ service: 'Amazon', message: textObject, count });
break;
@ -111,10 +136,8 @@ async function playVoice(filteredMessage, logoUrl, username, message) {
}
if (settings.MODULES.USE_CHATBUBBLE) {
socket.emit('xxx', currentLogoUrl, currentUsername, textObject);
socket.emit('xxx', message);
}
playNotificationSound();
}
module.exports = { playAudio, playVoice, playNotificationSound };

View file

@ -1,3 +1,5 @@
/* global settings, root, fs, settingsPath, ini */
function changeColor(section, setting, tempSection) {
document.querySelector(section).value = setting;
const value = document.querySelector(section).value;
@ -12,15 +14,11 @@ function setCurrentTheme(adjustTemp = false) {
changeColor('#TOP_BAR', settings.THEME.TOP_BAR, adjustTemp ? '--top-bar-temp' : '--top-bar');
changeColor('#MID_SECTION', settings.THEME.MID_SECTION, adjustTemp ? '--mid-section-temp' : '--mid-section');
changeColor('#CHAT_BUBBLE_BG', settings.THEME.CHAT_BUBBLE_BG, adjustTemp ? '--chat-bubble-temp' : '--chat-bubble');
changeColor(
'#CHAT_BUBBLE_HEADER',
settings.THEME.CHAT_BUBBLE_HEADER,
adjustTemp ? '--chat-bubble-header-temp' : '--chat-bubble-header',
);
changeColor('#CHAT_BUBBLE_HEADER', settings.THEME.CHAT_BUBBLE_HEADER, adjustTemp ? '--chat-bubble-header-temp' : '--chat-bubble-header');
changeColor(
'#CHAT_BUBBLE_MESSAGE',
settings.THEME.CHAT_BUBBLE_MESSAGE,
adjustTemp ? '--chat-bubble-message-temp' : '--chat-bubble-message',
adjustTemp ? '--chat-bubble-message-temp' : '--chat-bubble-message'
);
}
@ -50,7 +48,7 @@ document.body.querySelector('#MAIN_COLOR_1').addEventListener('input', () => {
document.body.querySelector('#MAIN_COLOR_1').addEventListener('change', () => {
settings.THEME.MAIN_COLOR_1 = document.getElementById('MAIN_COLOR_1').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#MAIN_COLOR_1', settings.THEME.MAIN_COLOR_1, '--main-color1');
});
@ -61,7 +59,7 @@ document.body.querySelector('#MAIN_COLOR_2').addEventListener('input', () => {
document.body.querySelector('#MAIN_COLOR_2').addEventListener('change', () => {
settings.THEME.MAIN_COLOR_2 = document.getElementById('MAIN_COLOR_2').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#MAIN_COLOR_2', settings.THEME.MAIN_COLOR_2, '--main-color2');
});
@ -72,7 +70,7 @@ document.body.querySelector('#MAIN_COLOR_3').addEventListener('input', () => {
document.body.querySelector('#MAIN_COLOR_3').addEventListener('change', () => {
settings.THEME.MAIN_COLOR_3 = document.getElementById('MAIN_COLOR_3').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#MAIN_COLOR_3', settings.THEME.MAIN_COLOR_3, '--main-color3');
});
@ -83,7 +81,7 @@ document.body.querySelector('#MAIN_COLOR_4').addEventListener('input', () => {
document.body.querySelector('#MAIN_COLOR_4').addEventListener('change', () => {
settings.THEME.MAIN_COLOR_4 = document.getElementById('MAIN_COLOR_4').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#MAIN_COLOR_4', settings.THEME.MAIN_COLOR_4, '--main-color4');
});
@ -94,7 +92,7 @@ document.body.querySelector('#TOP_BAR').addEventListener('input', () => {
document.body.querySelector('#TOP_BAR').addEventListener('change', () => {
settings.THEME.TOP_BAR = document.getElementById('TOP_BAR').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#TOP_BAR', settings.THEME.TOP_BAR, '--top-bar');
});
@ -105,7 +103,7 @@ document.body.querySelector('#MID_SECTION').addEventListener('input', () => {
document.body.querySelector('#MID_SECTION').addEventListener('change', () => {
settings.THEME.MID_SECTION = document.getElementById('MID_SECTION').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#MID_SECTION', settings.THEME.MID_SECTION, '--mid-section');
});
@ -116,7 +114,7 @@ document.body.querySelector('#CHAT_BUBBLE_BG').addEventListener('input', () => {
document.body.querySelector('#CHAT_BUBBLE_BG').addEventListener('change', () => {
settings.THEME.CHAT_BUBBLE_BG = document.getElementById('CHAT_BUBBLE_BG').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#CHAT_BUBBLE_BG', settings.THEME.CHAT_BUBBLE_BG, '--chat-bubble');
});
@ -127,7 +125,7 @@ document.body.querySelector('#CHAT_BUBBLE_HEADER').addEventListener('input', ()
document.body.querySelector('#CHAT_BUBBLE_HEADER').addEventListener('change', () => {
settings.THEME.CHAT_BUBBLE_HEADER = document.getElementById('CHAT_BUBBLE_HEADER').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#CHAT_BUBBLE_HEADER', settings.THEME.CHAT_BUBBLE_HEADER, '--chat-bubble-header');
});
@ -138,7 +136,7 @@ document.body.querySelector('#CHAT_BUBBLE_MESSAGE').addEventListener('input', ()
document.body.querySelector('#CHAT_BUBBLE_MESSAGE').addEventListener('change', () => {
settings.THEME.CHAT_BUBBLE_MESSAGE = document.getElementById('CHAT_BUBBLE_MESSAGE').value;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
changeColor('#CHAT_BUBBLE_MESSAGE', settings.THEME.CHAT_BUBBLE_MESSAGE, '--chat-bubble-message');
});

View file

@ -0,0 +1,392 @@
let __assign =
(this && this.__assign) ||
function () {
__assign =
Object.assign ||
function (t) {
for (let s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (const p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
let tokens = [];
const TokenAutocomplete = /** @class */ (function () {
function TokenAutocomplete(options) {
this.KEY_BACKSPACE = 8;
this.KEY_ENTER = 13;
this.KEY_UP = 38;
this.KEY_DOWN = 40;
this.tokens = tokens;
this.defaults = {
name: '',
selector: '',
noMatchesText: null,
initialTokens: null,
initialSuggestions: null,
suggestionsUri: '',
suggestionRenderer: TokenAutocomplete.Autocomplete.defaultRenderer,
minCharactersForSuggestion: 1
};
this.options = __assign(__assign({}, this.defaults), options);
const passedContainer = document.querySelector(this.options.selector);
if (!passedContainer) {
throw new Error('passed selector does not point to a DOM element.');
}
this.container = passedContainer;
this.container.classList.add('token-autocomplete-container');
if (!Array.isArray(this.options.initialTokens) && !Array.isArray(this.options.initialSuggestions)) {
this.parseTokensAndSuggestions();
}
this.hiddenSelect = document.createElement('select');
this.hiddenSelect.id = this.container.id + '-select';
this.hiddenSelect.name = this.options.name;
this.hiddenSelect.setAttribute('multiple', 'true');
this.hiddenSelect.style.display = 'none';
this.textInput = document.createElement('span');
this.textInput.id = this.container.id + '-input';
this.textInput.classList.add('token-autocomplete-input');
this.textInput.setAttribute('data-placeholder', 'Enter BetterTTV channel');
this.textInput.contentEditable = 'true';
this.container.appendChild(this.textInput);
this.container.appendChild(this.hiddenSelect);
this.select = new TokenAutocomplete.MultiSelect(this);
this.autocomplete = new TokenAutocomplete.Autocomplete(this);
this.debug(false);
const me = this;
if (Array.isArray(this.options.initialTokens)) {
this.options.initialTokens.forEach(function (token) {
if (typeof token === 'object') {
me.select.addToken(token.value, token.text);
}
});
}
this.textInput.addEventListener('keydown', function (event) {
if (event.which === me.KEY_ENTER || event.key === me.KEY_ENTER) {
event.preventDefault();
const highlightedSuggestion = me.autocomplete.suggestions.querySelector('.token-autocomplete-suggestion-highlighted');
if (highlightedSuggestion !== null) {
if (highlightedSuggestion.classList.contains('token-autocomplete-suggestion-active')) {
me.select.removeTokenWithText(highlightedSuggestion.textContent);
} else {
me.select.addToken(highlightedSuggestion.getAttribute('data-value'), highlightedSuggestion.textContent);
}
} else {
me.select.addToken(me.textInput.textContent, me.textInput.textContent);
}
me.clearCurrentInput();
} else if (me.textInput.textContent === '' && (event.which === me.KEY_BACKSPACE || event.key === me.KEY_BACKSPACE)) {
event.preventDefault();
// me.select.removeLastToken();
}
});
this.textInput.addEventListener('keyup', function (event) {
let _a, _b;
if ((event.which === me.KEY_UP || event.key === me.KEY_UP) && me.autocomplete.suggestions.childNodes.length > 0) {
const highlightedSuggestion = me.autocomplete.suggestions.querySelector('.token-autocomplete-suggestion-highlighted');
const aboveSuggestion = (_a = highlightedSuggestion) === null || _a === void 0 ? void 0 : _a.previousSibling;
if (aboveSuggestion != null) {
me.autocomplete.highlightSuggestion(aboveSuggestion);
}
return;
}
if ((event.which === me.KEY_DOWN || event.key === me.KEY_DOWN) && me.autocomplete.suggestions.childNodes.length > 0) {
const highlightedSuggestion = me.autocomplete.suggestions.querySelector('.token-autocomplete-suggestion-highlighted');
const belowSuggestion = (_b = highlightedSuggestion) === null || _b === void 0 ? void 0 : _b.nextSibling;
if (belowSuggestion != null) {
me.autocomplete.highlightSuggestion(belowSuggestion);
}
return;
}
me.autocomplete.hideSuggestions();
me.autocomplete.clearSuggestions();
const value = me.textInput.textContent || '';
if (value.length >= me.options.minCharactersForSuggestion) {
if (Array.isArray(me.options.initialSuggestions)) {
me.options.initialSuggestions.forEach(function (suggestion) {
if (typeof suggestion !== 'object') {
// the suggestion is of wrong type and therefore ignored
return;
}
if (value.localeCompare(suggestion.text.slice(0, value.length), undefined, { sensitivity: 'base' }) === 0) {
// The suggestion starts with the query text the user entered and will be displayed
me.autocomplete.addSuggestion(suggestion);
}
});
if (me.autocomplete.suggestions.childNodes.length > 0) {
me.autocomplete.highlightSuggestionAtPosition(0);
} else if (me.options.noMatchesText) {
me.autocomplete.addSuggestion({ value: '_no_match_', text: me.options.noMatchesText, description: null });
}
} else if (me.options.suggestionsUri.length > 0) {
me.autocomplete.requestSuggestions(value);
}
}
});
this.container.tokenAutocomplete = this;
}
/**
* Searches the element given as a container for option elements and creates active tokens (when the option is marked selected)
* and suggestions (all options found) from these. During this all found options are removed from the DOM.
*/
TokenAutocomplete.prototype.parseTokensAndSuggestions = function () {
const initialTokens = [];
const initialSuggestions = [];
const options = this.container.querySelectorAll('option');
const me = this;
options.forEach(function (option) {
if (option.text != null) {
if (option.hasAttribute('selected')) {
initialTokens.push({ value: option.value, text: option.text });
}
initialSuggestions.push({ value: option.value, text: option.text, description: null });
}
me.container.removeChild(option);
});
if (initialTokens.length > 0) {
this.options.initialTokens = initialTokens;
}
if (initialSuggestions.length > 0) {
this.options.initialSuggestions = initialSuggestions;
}
};
/**
* Clears the currently present tokens and creates new ones from the given input value.
*
* @param {(Array\|string)} value - either the name of a single token or a list of tokens to create
*/
TokenAutocomplete.prototype.val = function (value) {
this.select.clear();
if (Array.isArray(value)) {
const me1 = this;
value.forEach(function (token) {
if (typeof token === 'object') {
me1.select.addToken(token.value, token.text);
}
});
} else {
this.select.addToken(value.value, value.text);
}
};
TokenAutocomplete.prototype.clearCurrentInput = function () {
this.textInput.textContent = '';
};
TokenAutocomplete.prototype.debug = function (state) {
if (state) {
this.log = console.log.bind(window.console);
} else {
this.log = function () {};
}
};
let _a;
TokenAutocomplete.MultiSelect = /** @class */ (function () {
function saveTokens() {
settings.TWITCH.BETTERTTV_CHANNELS = tokens;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
}
function class1(parent) {
this.parent = parent;
this.container = parent.container;
this.options = parent.options;
}
/**
* Adds a token with the specified name to the list of currently prensent tokens displayed to the user and the hidden select.
*
* @param {string} tokenText - the name of the token to create
*/
class1.prototype.addToken = function (tokenValue, tokenText) {
if (tokenValue === null || tokenText === null) {
return;
}
tokens.push({ value: tokenValue, text: tokenText });
saveTokens();
const option = document.createElement('option');
option.text = tokenText;
option.value = tokenValue;
option.setAttribute('selected', 'true');
option.setAttribute('data-text', tokenText);
option.setAttribute('data-value', tokenValue);
this.parent.hiddenSelect.add(option);
const token = document.createElement('span');
token.classList.add('token-autocomplete-token');
token.setAttribute('data-text', tokenText);
option.setAttribute('data-value', tokenValue);
token.textContent = tokenText;
const deleteToken = document.createElement('span');
deleteToken.classList.add('token-autocomplete-token-delete');
deleteToken.textContent = '\u00D7';
token.appendChild(deleteToken);
const me = this;
deleteToken.addEventListener('click', function (event) {
me.removeToken(token);
});
this.container.insertBefore(token, this.parent.textInput);
this.parent.log('added token', token);
};
/**
* Completely clears the currently present tokens from the field.
*/
class1.prototype.clear = function () {
const tokens = this.container.querySelectorAll('.token-autocomplete-token');
const me = this;
tokens.forEach(function (token) {
me.removeToken(token);
});
};
/**
* Removes the last token in the list of currently present token. This is the last added token next to the input field.
*/
class1.prototype.removeLastToken = function () {
const tokens = this.container.querySelectorAll('.token-autocomplete-token');
const token = tokens[tokens.length - 1];
if (tokens.length !== 0) {
this.removeToken(token);
}
};
/**
* Removes the specified token from the list of currently present tokens.
*
* @param {Element} token - the token to remove
*/
class1.prototype.removeToken = function (token) {
let _a, _b;
this.container.removeChild(token);
const tokenText = token.getAttribute('data-text');
const hiddenOption = this.parent.hiddenSelect.querySelector('option[data-text="' + tokenText + '"]');
(_b = (_a = hiddenOption) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0
? void 0
: _b.removeChild(hiddenOption);
this.parent.log('removed token', token.textContent);
tokens.splice(
tokens.findIndex(t => t.value === token.value),
1
);
saveTokens();
};
class1.prototype.removeTokenWithText = function (tokenText) {
if (tokenText === null) {
return;
}
const token = this.container.querySelector('.token-autocomplete-token[data-text="' + tokenText + '"]');
if (token !== null) {
this.removeToken(token);
}
};
return class1;
})();
TokenAutocomplete.Autocomplete =
((_a = /** @class */ (function () {
function class2(parent) {
this.parent = parent;
this.container = parent.container;
this.options = parent.options;
this.renderer = parent.options.suggestionRenderer;
this.suggestions = document.createElement('ul');
this.suggestions.id = this.container.id + '-suggestions';
this.suggestions.classList.add('token-autocomplete-suggestions');
this.container.appendChild(this.suggestions);
}
/**
* Hides the suggestions dropdown from the user.
*/
class2.prototype.hideSuggestions = function () {
this.suggestions.style.display = '';
};
/**
* Shows the suggestions dropdown to the user.
*/
class2.prototype.showSuggestions = function () {
this.suggestions.style.display = 'block';
};
class2.prototype.highlightSuggestionAtPosition = function (index) {
const suggestions = this.suggestions.querySelectorAll('li');
suggestions.forEach(function (suggestion) {
suggestion.classList.remove('token-autocomplete-suggestion-highlighted');
});
suggestions[index].classList.add('token-autocomplete-suggestion-highlighted');
};
class2.prototype.highlightSuggestion = function (suggestion) {
this.suggestions.querySelectorAll('li').forEach(function (suggestion) {
suggestion.classList.remove('token-autocomplete-suggestion-highlighted');
});
suggestion.classList.add('token-autocomplete-suggestion-highlighted');
};
/**
* Removes all previous suggestions from the dropdown.
*/
class2.prototype.clearSuggestions = function () {
this.suggestions.innerHTML = '';
};
/**
* Loads suggestions matching the given query from the rest service behind the URI given as an option while initializing the field.
*
* @param query the query to search suggestions for
*/
class2.prototype.requestSuggestions = function (query) {
const me = this;
const request = new XMLHttpRequest();
request.onload = function () {
if (Array.isArray(request.response)) {
request.response.forEach(function (suggestion) {
me.addSuggestion(suggestion);
});
}
};
request.open('GET', me.options.suggestionsUri + '?query=' + query, true);
request.responseType = 'json';
request.setRequestHeader('Content-type', 'application/json');
request.send();
};
/**
* Adds a suggestion with the given text matching the users input to the dropdown.
*
* @param {string} suggestionText - the text that should be displayed for the added suggestion
*/
class2.prototype.addSuggestion = function (suggestion) {
const element = this.renderer(suggestion);
element.setAttribute('data-value', suggestion.value);
const me = this;
element.addEventListener('click', function (_event) {
if (suggestion.text === me.options.noMatchesText) {
return true;
}
if (element.classList.contains('token-autocomplete-suggestion-active')) {
me.parent.select.removeTokenWithText(suggestion.text);
} else {
me.parent.select.addToken(suggestion.value, suggestion.text);
}
me.clearSuggestions();
me.hideSuggestions();
me.parent.clearCurrentInput();
});
if (this.container.querySelector('.token-autocomplete-token[data-text="' + suggestion.text + '"]') !== null) {
element.classList.add('token-autocomplete-suggestion-active');
}
this.suggestions.appendChild(element);
this.showSuggestions();
me.parent.log('added suggestion', suggestion);
};
return class2;
})()),
(_a.defaultRenderer = function (suggestion) {
const option = document.createElement('li');
option.textContent = suggestion.text;
if (suggestion.description) {
const description = document.createElement('small');
description.textContent = suggestion.description;
description.classList.add('token-autocomplete-suggestion-description');
option.appendChild(description);
}
return option;
}),
_a);
return TokenAutocomplete;
})();
module.exports = { TokenAutocomplete };

344
src/js/trovo.js Normal file
View file

@ -0,0 +1,344 @@
/* global client,axios,customEmojiList, saveCustomEmotesToFile,startTime,Sockette, playNotificationSound, chat, replaceChatMessageWithCustomEmojis, messageId, addSingleTooltip, settingsPath, fs, ini, backend, main, path, resourcesPath, customEmojis, emojiPicker,config, settings, options, sound, showChatMessage, messageTemplates, getPostTime */
function setTrovoSendButton() {
const languageSelectContent = document.querySelector('.send-to-channel');
const option = document.createElement('div');
option.classList = 'language-select';
const checkbox = document.createElement('input');
checkbox.classList = 'checkbox';
checkbox.type = 'checkbox';
checkbox.id = 'SEND_CHAT_TROVO';
option.appendChild(checkbox);
const label = document.createElement('label');
label.classList = 'toggle-small';
option.setAttribute('for', 'SEND_CHAT_TROVO');
checkbox.checked = settings.TROVO.SEND_CHAT;
option.appendChild(label);
const network = document.createElement('img');
network.src = './images/trovo.png';
network.classList = 'emote';
option.appendChild(network);
option.addEventListener('click', () => {
checkbox.checked = !checkbox.checked;
settings.TROVO.SEND_CHAT = checkbox.checked;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
});
languageSelectContent.appendChild(option);
}
setTrovoSendButton();
function getTrovoUserId() {
// Get user Logo with access token
options = {
method: 'GET',
url: 'https://open-api.trovo.live/openplatform/validate',
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID,
Authorization: `OAuth ${settings.TROVO.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(response => {
settings.TROVO.USERNAME = response.data.nick_name;
settings.TROVO.USER_ID = response.data.uid;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
getTrovoUserInfo();
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
function getTrovoUserInfo() {
// Get user Logo with access token
options = {
method: 'GET',
url: 'https://open-api.trovo.live/openplatform/getuserinfo',
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID,
Authorization: `OAuth ${settings.TROVO.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(response => {
settings.TROVO.USER_LOGO_URL = response.data.profilePic;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
function saveTrovoEmotesToFile(TrovoEmotes) {
const data = JSON.stringify(TrovoEmotes);
const savePath =
main.isPackaged === true ? path.join(resourcesPath, './custom-emotes.json') : path.join(resourcesPath, './config/custom-emotes.json');
fs.writeFile(savePath, data, error => {
if (error) {
console.error(error);
throw error;
}
});
}
function formatTrovoEmotes(data) {
if (data.channels.customizedEmotes.channel) {
data.channels.customizedEmotes.channel.forEach(channel => {
channel.emotes.forEach(emote => {
const emojiToBeAdded = {
name: ':' + emote.name,
shortcodes: [':' + emote.name],
url: emote.url,
category: 'Trovo ' + settings.TROVO.CHANNEL_NAME
};
customEmojis.push(emojiToBeAdded);
});
});
}
// data.channels.eventEmotes.forEach(emote => {
// const emojiToBeAdded = {
// name: emote.name,
// shortcodes: [emote.name],
// url: emote.url,
// category: 'Trovo event emotes'
// };
// customEmojis.push(emojiToBeAdded);
// });
if (data.channels.globalEmotes) {
data.channels.globalEmotes.forEach(emote => {
const emojiToBeAdded = {
name: ':' + emote.name,
shortcodes: [':' + emote.name],
url: emote.url,
category: 'Trovo Global'
};
customEmojis.push(emojiToBeAdded);
});
}
customEmojiList = [...customEmojis];
emojiPicker.customEmoji = customEmojiList;
saveCustomEmotesToFile(customEmojis);
}
function getTrovoEmotes() {
// Get user Logo with access token
options = {
method: 'POST',
url: 'https://open-api.trovo.live/openplatform/getemotes',
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID,
Authorization: `OAuth ${settings.TROVO.OAUTH_TOKEN}`
},
data: {
emote_type: 0,
channel_id: [settings.TROVO.CHANNEL_ID]
}
};
axios
.request(options)
.then(response => {
formatTrovoEmotes(response.data);
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
function getTrovoChannelId() {
options = {
method: 'POST',
url: 'https://open-api.trovo.live/openplatform/getusers',
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID
},
data: { user: [settings.TROVO.CHANNEL_NAME.toLowerCase()] }
};
axios
.request(options)
.then(response => {
settings.TROVO.CHANNEL_ID = response.data.users[0].channel_id;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
getTrovoEmotes();
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
function sendTrovoMessage(message) {
options = {
method: 'POST',
url: 'https://open-api.trovo.live/openplatform/chat/send',
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID,
Authorization: `OAuth ${settings.TROVO.OAUTH_TOKEN}`
},
data: { content: message, channel_id: settings.TROVO.CHANNEL_ID }
};
axios
.request(options)
.then(response => {
console.log(response);
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
const getTrovoChatToken = () =>
new Promise(resolve => {
options = {
method: 'GET',
url: `https://open-api.trovo.live/openplatform/chat/channel-token/${settings.TROVO.CHANNEL_ID}`,
headers: {
Accept: 'application/json',
'Client-ID': settings.TROVO.CLIENT_ID
}
};
axios
.request(options)
.then(response => {
resolve(response.data.token);
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
});
async function displayTrovoMessage(message) {
messageId++;
const article = document.createElement('article');
article.className = 'msg-container sender';
article.setAttribute('id', messageId);
article.innerHTML = messageTemplates.trovoTemplate;
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = message.avatar;
userImg.setAttribute('tip', '');
}
addSingleTooltip(userImg);
const usernameHtml = article.querySelector('.username');
if (usernameHtml) {
usernameHtml.innerText = message.nick_name;
}
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = getPostTime();
}
article.appendChild(postTime);
const formattedMessage = article.querySelector('.msg-box');
if (formattedMessage) {
formattedMessage.innerHTML = message.content;
}
await chat.replaceChatMessageWithCustomEmojis(formattedMessage.innerHTML).then(data => {
formattedMessage.innerHTML = data;
showChatMessage(article);
});
}
async function connectToTrovoChat(token) {
let startTime = new Date();
let heartbeat = null;
const wsTrovo = new Sockette('wss://open-chat.trovo.live/chat', {
onopen: e => {
// console.log('Connected!', e);
wsTrovo.json({ type: 'AUTH', nonce: 'loquendoBot', data: { token } });
},
onmessage: e => {
// console.log('Received:', e);
const data = JSON.parse(e.data);
// console.log(data);
if (data.type === 'RESPONSE' && !data.error) {
wsTrovo.json({ type: 'PING', nonce: 'loquendoBot' });
heartbeat = setInterval(() => {
wsTrovo.json({ type: 'PING', nonce: 'loquendoBot' });
}, 30e3);
}
if (data.type === 'CHAT' && data.data.chats) {
data.data.chats.forEach(message => {
// console.log(message);
if (message.type === 5007) {
return;
}
if (Math.round(startTime.getTime() / 1000) < message.send_time) {
displayTrovoMessage({
nick_name: message.nick_name,
avatar: message.avatar,
content: message.content
});
}
});
}
if (data.type === 'PONG') {
clearInterval(heartbeat);
heartbeat = setInterval(() => {
wsTrovo.json({ type: 'PING', nonce: 'loquendoBot' });
}, data.data.gap * 1000);
}
},
onreconnect: e => {
console.log('Reconnecting...', e);
startTime = new Date();
clearInterval(heartbeat);
getTrovoChatToken().then(data => {
token = data;
});
},
onmaximum: e => console.log('Stop Attempting!', e),
onclose: e => console.log('Closed!', e),
onerror: e => console.log('Error:', e)
});
// ws.close(); // graceful shutdown
}
if (settings.TROVO.CHANNEL_ID) {
getTrovoChatToken().then(data => {
connectToTrovoChat(data);
});
}
module.exports = { getTrovoUserId, getTrovoChannelId, sendTrovoMessage };

View file

@ -1,106 +1,189 @@
const tmi = require('tmi.js');
const axios = require('axios');
/* global client, axios, playNotificationSound, betterTTVAutocomplete, saveCustomEmotesToFile, customEmojiList, chat, replaceChatMessageWithCustomEmojis, messageId, addSingleTooltip, settingsPath, fs, ini, backend, main, path, resourcesPath, customEmojis, emojiPicker,config, settings, options, sound, showChatMessage, messageTemplates, getPostTime */
let client;
const tmi = require('tmi.js');
let client = null;
let logoUrl = null;
const twitchChannels = [];
function sendMessage(message) {
client.say(settings.TWITCH.CHANNEL_NAME, message).catch(console.error);
}
client = new tmi.Client({
if (settings.TWITCH.USERNAME && settings.TWITCH.OAUTH_TOKEN) {
client = new tmi.Client({
options: {
skipUpdatingEmotesets: true,
skipUpdatingEmotesets: true
},
identity: {
username: settings.TWITCH.USERNAME,
password: settings.TWITCH.OAUTH_TOKEN,
password: settings.TWITCH.OAUTH_TOKEN
},
channels: [settings.TWITCH.CHANNEL_NAME],
});
channels: [settings.TWITCH.CHANNEL_NAME]
});
client
client
.connect()
.then((data) => {})
.then(data => {})
.catch(console.error);
client.on('message', (channel, tags, message, self) => {
if (self || tags['display-name'] === settings.TWITCH.USERNAME) {
return;
}
const emotes = tags.emotes || {};
const emoteValues = Object.entries(emotes);
let filteredMessage = message;
let emoteMessage = message;
emoteValues.forEach(entry => {
entry[1].forEach(lol => {
const [start, end] = lol.split('-');
const emote = `<img src="https://static-cdn.jtvnw.net/emoticons/v2/${entry[0]}/default/dark/1.0"/>`;
emoteMessage = emoteMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), emote);
filteredMessage = filteredMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), '');
});
});
const messageObject = parseString(emoteMessage);
getProfileImage(tags['user-id'], tags['display-name'], messageObject, filteredMessage);
});
}
function checkIfTokenIsValid() {
options = {
method: 'GET',
url: 'https://id.twitch.tv/oauth2/validate',
headers: {
Authorization: `OAuth ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(response => {
document.getElementById('TWITCH_CHANNEL_NAME').disabled = false;
})
.catch(error => {
console.error(error);
document.getElementById('TWITCH_CHANNEL_NAME').disabled = true;
config.createNotification('Oauth Token is invalid, please obtain a new one', 'error');
});
}
setInterval(checkIfTokenIsValid, 600000);
if (settings.TWITCH.OAUTH_TOKEN) {
checkIfTokenIsValid();
} else {
document.getElementById('TWITCH_CHANNEL_NAME').disabled = true;
}
function ping(element) {
let value = document.body.querySelector(element);
const value = document.body.querySelector(element);
client
.ping()
.then((data) => {
.then(data => {
value.classList.add('success');
value.innerText = 'Success!';
})
.catch((e) => {
.catch(e => {
value.classList.add('error');
value.innerText = 'Failed!';
});
}
function displayTwitchMessage(logoUrl, username, messageObject, fileteredMessage) {
async function displayTwitchMessage(logoUrl, username, messageObject, filteredMessage) {
messageId++;
const article = document.createElement('article');
article.className = 'msg-container msg-remote';
article.className = 'msg-container sender';
article.setAttribute('id', messageId);
article.innerHTML = messageTemplates.twitchTemplate;
const userImg = article.querySelector('.icon-container > .user-img');
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = logoUrl;
userImg.setAttribute('tip', '');
}
addSingleTooltip(userImg);
const usernameHtml = article.querySelector('.username');
if (usernameHtml) {
usernameHtml.innerText = username;
}
const postTime = document.createElement('span');
postTime.classList.add('post-time');
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = getPostTime();
}
const iconContainer = article.querySelector('.icon-container');
iconContainer.appendChild(postTime);
article.appendChild(postTime);
const msg = article.querySelector('.msg-box');
if (msg) {
messageObject.forEach((entry) => {
const formattedMessage = article.querySelector('.msg-box');
if (formattedMessage) {
messageObject.forEach(entry => {
if (entry.text) {
msg.innerHTML += entry.text;
formattedMessage.innerHTML += entry.text;
} else {
msg.innerHTML += entry.html;
formattedMessage.innerHTML += entry.html;
}
});
// msg.appendChild(postTime);
}
// Appends the message to the main chat box (shows the message)
showChatMessage(article, false);
await chat.replaceChatMessageWithCustomEmojis(formattedMessage.innerHTML).then(data => {
formattedMessage.innerHTML = data;
showChatMessage(article);
});
if (fileteredMessage) {
sound.playVoice(fileteredMessage, logoUrl, username, msg);
if (settings.LANGUAGE.USE_DETECTION && filteredMessage.length > 20 && countWords(filteredMessage) > 4) {
await backend.getDetectedLanguage({ message: filteredMessage, messageId, username, logoUrl, formattedMessage }).then(language => {
const languageElement = document.createElement('span');
languageElement.classList = `fi fi-${language.ISO3166} fis language-icon flag-icon`;
languageElement.setAttribute('tip', language.name);
article.appendChild(languageElement);
addSingleTooltip(languageElement);
if (filteredMessage && !settings.LANGUAGE.OUTPUT_TO_TTS) {
sound.playVoice({
filteredMessage,
logoUrl,
username,
formattedMessage,
language: { selectedLanguage: null, detectedLanguage: language }
});
}
window.article = article;
// window.article = article;
});
} else {
if (filteredMessage) {
sound.playVoice({ filteredMessage, logoUrl, username, formattedMessage });
}
// window.article = article;
}
sound.playNotificationSound();
}
function getProfileImage(userid, username, message, fileteredMessage) {
function getProfileImage(userid, username, message, filteredMessage) {
// Get user Logo with access token
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/users?id=${userid}`,
headers: { 'Client-ID': settings.TWITCH.CLIENT_ID, Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}` },
headers: { 'Client-ID': settings.TWITCH.CLIENT_ID, Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}` }
};
axios
.request(options)
.then((responseLogoUrl) => {
const logoUrl = responseLogoUrl.data.data[0].profile_image_url;
displayTwitchMessage(logoUrl, username, message, fileteredMessage);
.then(responseLogoUrl => {
logoUrl = responseLogoUrl.data.data[0].profile_image_url;
displayTwitchMessage(logoUrl, username, message, filteredMessage);
})
.catch((error) => {
.catch(error => {
console.error(error);
});
}
@ -122,26 +205,303 @@ function parseString(inputString) {
return result;
}
client.on('message', (channel, tags, message, self) => {
if (self) {
function formatTwitchEmotes(channel) {
if (channel.emotes.length === 0) {
return;
}
const emotes = tags.emotes || {};
const emoteValues = Object.entries(emotes);
let fileteredMessage = message;
let emoteMessage = message;
emoteValues.forEach((entry) => {
entry[1].forEach((lol) => {
const [start, end] = lol.split('-');
let emote = `<img src="https://static-cdn.jtvnw.net/emoticons/v2/${entry[0]}/default/dark/1.0"/>`;
emoteMessage = emoteMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), emote);
fileteredMessage = fileteredMessage.replaceAll(message.slice(parseInt(start), parseInt(end) + 1), '');
channel.emotes.forEach(emote => {
if (channel.name !== 'Twitch Global' && emote.emote_type === 'bitstier') {
return;
}
if (channel.name !== 'Twitch Global' && emote.emote_type === 'subscriptions' && parseInt(channel.tier) < parseInt(emote.tier)) {
return;
}
if (channel.name !== 'Twitch Global' && emote.emote_type === 'follower ' && parseInt(channel.tier) === 0) {
return;
}
const emojiToBeAdded = {
name: emote.name,
shortcodes: [emote.name],
url: emote.images.url_1x,
category: 'Twitch ' + channel.broadcaster_name
};
customEmojis.push(emojiToBeAdded);
});
customEmojiList = [...customEmojis];
emojiPicker.customEmoji = customEmojiList;
saveCustomEmotesToFile(customEmojis);
}
function formatBetterTtvEmotes(data) {
if (data.emotes.length === 0) {
return;
}
data.emotes.forEach(emote => {
const emojiToBeAdded = {
name: emote.code,
shortcodes: [emote.code],
url: `https://cdn.betterttv.net/emote/${emote.id}/1x.webp`,
category: data.name
};
customEmojis.push(emojiToBeAdded);
});
customEmojiList = [...customEmojis];
emojiPicker.customEmoji = customEmojiList;
saveCustomEmotesToFile(customEmojis);
}
function getBetterTtvGLobalEmotes() {
// Get user Logo with access token
options = {
method: 'GET',
url: 'https://api.betterttv.net/3/cached/emotes/global',
headers: {}
};
axios
.request(options)
.then(response => {
formatBetterTtvEmotes({ name: 'BetterTTV Global', emotes: response.data });
})
.catch(error => {
console.error(error);
});
}
function getBetterTtvChannelsEmotes() {
betterTTVAutocomplete.tokens.forEach(channel => {
getTwitchChannelId(channel.value).then(channelId => {
getBetterTtvChannelEmotes(channelId);
});
});
}
function getBetterTtvChannelEmotes(channel) {
// Get user Logo with access token
options = {
method: 'GET',
url: `https://api.betterttv.net/3/cached/users/twitch/${channel}`,
headers: {}
};
axios
.request(options)
.then(response => {
formatBetterTtvEmotes({ name: 'BetterTTV Channels', emotes: [...response.data.channelEmotes, ...response.data.sharedEmotes] });
})
.catch(error => {
console.error(error);
});
}
function getTwitchUserFollows(paginationToken) {
let url = '';
if (!paginationToken) {
url = `https://api.twitch.tv/helix/channels/followed?user_id=${settings.TWITCH.USER_ID}&first=100`;
} else {
url = `https://api.twitch.tv/helix/channels/followed?user_id=${settings.TWITCH.USER_ID}&after=${paginationToken}`;
}
options = {
method: 'GET',
url: url,
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
// console.log(responseLogoUrl);
responseLogoUrl.data.data.forEach(channel => {
twitchChannels.push({
broadcaster_id: channel.broadcaster_id,
broadcaster_name: channel.broadcaster_name,
tier: '0'
});
});
let messageObject = parseString(emoteMessage);
getProfileImage(tags['user-id'], tags['display-name'], messageObject, fileteredMessage);
});
if (Object.keys(responseLogoUrl.data.pagination).length !== 0) {
getTwitchUserFollows(responseLogoUrl.data.pagination.cursor);
} else {
getTwitchChannelSubscriptions(twitchChannels);
}
})
.catch(error => {
console.error(error);
});
}
module.exports = { sendMessage, ping, client };
function getTwitchChannelSubscriptions(channels) {
channels.forEach(channel => {
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/subscriptions/user?broadcaster_id=${channel.broadcaster_id}&user_id=${settings.TWITCH.USER_ID}`,
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
const objIndex = twitchChannels.findIndex(obj => obj.broadcaster_id === channel.broadcaster_id);
twitchChannels[objIndex].tier = responseLogoUrl.data.data[0].tier;
getTwitchChannelEmotes(channel);
})
.catch(error => {
if (error.response.status !== 404) {
console.error(error);
}
});
});
}
function getTwitchChannelEmotes(channel) {
// Get user Logo with access token
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/chat/emotes?broadcaster_id=${channel.broadcaster_id}`,
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
if (responseLogoUrl.data.data.length !== 0) {
channel.emotes = responseLogoUrl.data.data;
formatTwitchEmotes(channel);
}
})
.catch(error => {
console.error(error);
});
}
function getTwitchGlobalEmotes() {
// Get user Logo with access token
options = {
method: 'GET',
url: 'https://api.twitch.tv/helix/chat/emotes/global',
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
formatTwitchEmotes({ broadcaster_name: 'Global', emotes: responseLogoUrl.data.data });
})
.catch(error => {
console.error(error);
});
}
async function getUserAvailableTwitchEmotes() {
if (settings.TWITCH.OAUTH_TOKEN) {
await getTwitchGlobalEmotes();
await getTwitchUserFollows();
await getTwitchChannelEmotes({
broadcaster_id: settings.TWITCH.USER_ID,
broadcaster_name: settings.TWITCH.USERNAME,
tier: '3000'
});
await getTwitchChannelEmotes({
broadcaster_id: settings.TWITCH.CHANNEL_USER_ID,
broadcaster_name: settings.TWITCH.CHANNEL_NAME,
tier: '1'
});
}
}
const getTwitchChannelId = channelName =>
new Promise(resolve => {
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/users?login=${channelName}`,
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(response => {
resolve(response.data.data[0].id);
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain channel info, please try again', 'error');
});
});
function getTwitchUserId() {
// Get user Logo with access token
options = {
method: 'GET',
url: 'https://api.twitch.tv/helix/users',
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
// console.log(responseLogoUrl.data.data[0]);
settings.TWITCH.USERNAME = responseLogoUrl.data.data[0].display_name;
settings.TWITCH.USER_LOGO_URL = responseLogoUrl.data.data[0].profile_image_url;
settings.TWITCH.USER_ID = responseLogoUrl.data.data[0].id;
fs.writeFileSync(settingsPath, JSON.stringify(settings));
config.createNotification('Obtained user info succesfully', 'success');
})
.catch(error => {
console.error(error);
config.createNotification('could not obtain user info, please try again', 'error');
});
}
// const Sockette = require('sockette');
// const ws = new Sockette('wss://eventsub.wss.twitch.tv/ws', {
// timeout: 5e3,
// maxAttempts: 10,
// onopen: e => console.log('Connected!', e),
// onmessage: e => console.log('Received:', e),
// onreconnect: e => console.log('Reconnecting...', e),
// onmaximum: e => console.log('Stop Attempting!', e),
// onclose: e => console.log('Closed!', e),
// onerror: e => console.log('Error:', e)
// });
// ws.send('Hello, world!');
// ws.json({ type: 'ping' });
// ws.close(); // graceful shutdown
// Reconnect 10s later
// setTimeout(ws.reconnect, 10e3);
module.exports = {
sendMessage,
ping,
client,
getBetterTtvGLobalEmotes,
getBetterTtvChannelEmotes,
getBetterTtvChannelsEmotes,
getUserAvailableTwitchEmotes,
getTwitchChannelId,
getTwitchUserId,
checkIfTokenIsValid
};

96
src/js/youtube.js Normal file
View file

@ -0,0 +1,96 @@
/* global settings, bot, messageId, showChatMessage, messageTemplates, chat, addSingleTooltip,getPostTime */
const { LiveChat } = require('youtube-chat');
const startTime = new Date();
// If channelId is specified, liveId in the current stream is automatically acquired.
// Recommended
// const liveChat = new LiveChat({ handle: settings.YOUTUBE.CHANNEL_HANDLE });
const liveChat = new LiveChat({ liveId: settings.YOUTUBE.LIVE_ID });
// Or specify LiveID in Stream manually.
// const liveChat = new LiveChat({ liveId: '4xDzrJKXOOY' });
// Emit at start of observation chat.
// liveId: string
liveChat.on('start', liveId => {
/* Your code here! */
console.log(liveId);
});
// Emit at end of observation chat.
// reason: string?
liveChat.on('end', reason => {
/* Your code here! */
console.log(reason);
});
// Emit at receive chat.
// chat: ChatItem
liveChat.on('chat', message => {
/* Your code here! */
if (message.timestamp > startTime) {
displayYoutubeMessage(message);
}
});
// Emit when an error occurs
// err: Error or any
liveChat.on('error', err => {
/* Your code here! */
console.log(err);
});
async function startYoutube() {
// Start fetch loop
const ok = await liveChat.start();
if (!ok) {
console.log('Failed to start, check emitted error');
}
}
startYoutube();
async function displayYoutubeMessage(message) {
messageId++;
const article = document.createElement('article');
article.className = 'msg-container sender';
article.setAttribute('id', messageId);
article.innerHTML = messageTemplates.youtubeTemplate;
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = message.author.thumbnail.url;
userImg.setAttribute('tip', '');
}
addSingleTooltip(userImg);
const usernameHtml = article.querySelector('.username');
if (usernameHtml) {
usernameHtml.innerText = message.author.name;
}
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = getPostTime();
}
article.appendChild(postTime);
const formattedMessage = article.querySelector('.msg-box');
if (formattedMessage) {
message.message.forEach(entry => {
if (entry.text) {
formattedMessage.innerHTML += entry.text;
} else {
formattedMessage.innerHTML += `<img src="${entry.url}"/>`;
}
});
}
await chat.replaceChatMessageWithCustomEmojis(formattedMessage.innerHTML).then(data => {
formattedMessage.innerHTML = data;
showChatMessage(article);
});
}

View file

@ -1,38 +1,33 @@
/* global pythonPath, a */
const { app, BrowserWindow, ipcMain } = require('electron');
const { writeIniFile } = require('write-ini-file');
const path = require('path');
const http = require('http');
const kill = require('kill-process-by-name');
const ini = require('ini');
const fs = require('fs');
let resourcesPath = __dirname;
let settingsPath;
let settingsPath = null;
let settings;
let window;
if (app.isPackaged) {
settingsPath = path.join(process.resourcesPath, './settings.ini');
settingsPath = path.join(process.resourcesPath, './settings.json');
pythonPath = path.join(process.resourcesPath, './backend');
resourcesPath = process.resourcesPath;
} else {
settingsPath = path.join(resourcesPath, './config/settings.ini');
settingsPath = path.join(resourcesPath, './config/settings.json');
pythonPath = path.join(resourcesPath, './backend');
}
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit();
}
async function createWindow() {
if (!fs.existsSync(settingsPath)) {
console.log(resourcesPath);
await createIniFile();
settings = await createSettingsFile();
} else {
settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8'));
const file = fs.readFileSync(settingsPath);
settings = JSON.parse(file);
}
window = new BrowserWindow({
@ -42,21 +37,41 @@ async function createWindow() {
x: parseInt(settings.GENERAL.POSITION_X),
y: parseInt(settings.GENERAL.POSITION_Y),
frame: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
});
window.loadFile(path.join(__dirname, 'index.html'));
// Create the second window
const secondWindow = new BrowserWindow({
width: 1920,
height: 1080,
// parent: window, // Set the parent window
// modal: true,
frame: false,
show: true, // Hide the second window initially if needed
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
backgroundThrottling: false
}
});
window.loadFile(path.join(__dirname, 'index.html'));
// Load second.html into the second window
secondWindow.loadFile(path.join(__dirname, './modules/facemask/index.html'));
// secondWindow.webContents.setFrameRate(60);
if (!app.isPackaged) {
window.webContents.openDevTools();
secondWindow.webContents.openDevTools();
}
window.on('close', (e) => {
settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8')); // load newest settings in case anything changed after starting the program
window.on('close', e => {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); // load newest settings in case anything changed after starting the program
const bounds = window.getBounds();
settings.GENERAL.WIDTH = bounds.width;
@ -64,16 +79,20 @@ async function createWindow() {
settings.GENERAL.POSITION_X = bounds.x;
settings.GENERAL.POSITION_Y = bounds.y;
fs.writeFileSync(settingsPath, ini.stringify(settings));
fs.writeFileSync(settingsPath, JSON.stringify(settings));
});
}
// app.disableHardwareAcceleration();
// app.disableDomainBlockingFor3DAPIs();
app.whenReady().then(() => {
createWindow();
});
app.on('window-all-closed', (event) => {
app.on('window-all-closed', event => {
if (process.platform !== 'darwin') {
// kill('loquendoBot_backend');
app.quit();
}
});
@ -86,6 +105,7 @@ app.on('activate', () => {
app.on('before-quit', () => {
window.webContents.send('quit-event');
// kill('loquendoBot_backend');
});
ipcMain.on('resize-window', (event, width, height) => {
@ -93,12 +113,12 @@ ipcMain.on('resize-window', (event, width, height) => {
browserWindow.setSize(width, height);
});
ipcMain.on('minimize-window', (event) => {
ipcMain.on('minimize-window', event => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
browserWindow.minimize();
});
ipcMain.on('maximize-window', (event) => {
ipcMain.on('maximize-window', event => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (!browserWindow.isMaximized()) {
@ -108,23 +128,23 @@ ipcMain.on('maximize-window', (event) => {
}
});
ipcMain.on('close-window', (event) => {
ipcMain.on('close-window', event => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
kill('loquendoBot_backend');
browserWindow.close();
app.quit();
});
ipcMain.on('restart', (event) => {
ipcMain.on('restart', event => {
app.relaunch();
});
ipcMain.on('environment', (event) => {
ipcMain.on('environment', event => {
event.returnValue = { resourcesPath, pythonPath, settingsPath, settings, isPackaged: app.isPackaged };
});
async function createIniFile() {
await writeIniFile(settingsPath, {
async function createSettingsFile() {
const settingsx = {
GENERAL: {
VOICE_ENABLED: true,
NOTIFICATION_ENABLED: true,
@ -132,36 +152,49 @@ async function createIniFile() {
POSITION_Y: 0,
WIDTH: 1024,
HEIGHT: 768,
LANGUAGE: 'EN',
LANGUAGE: 'none',
LANGUAGE_INDEX: '0',
PORT: 9000,
VIEWERS_PANEL: false,
LOCATION: pythonPath,
ZOOMLEVEL: 1
},
LANGUAGE: {
USE_DETECTION: false,
TRANSLATE_TO: 'none',
LANGUAGE_INDEX: '0',
BROADCAST_TRANSLATION: false,
OUTPUT_TO_TTS: false,
TRANSLATE_TO_INDEX: 0,
SEND_TRANSLATION: false,
SEND_TRANSLATION_IN: 'none',
SEND_TRANSLATION_OUT: 'none'
},
TTS: {
USE_TTS: true,
USE_TTS: false,
PRIMARY_VOICE: '',
PRIMARY_TTS_LANGUAGE: 'EN',
PRIMARY_TTS_LANGUAGE: 'none',
SECONDARY_VOICE: '',
SECONDARY_TTS_LANGUAGE: 'EN',
SECONDARY_TTS_LANGUAGE: 'none',
PRIMARY_TTS_LANGUAGE_INDEX: 0,
SECONDARY_TTS_LANGUAGE_INDEX: 0
},
STT: {
USE_STT: false,
MICROPHONE_ID: 'default',
SELECTED_MICROPHONE: 'default',
MICROPHONE: 0,
LANGUAGE: 'vosk-model-small-es-0.42',
LANGUAGE: ''
},
AUDIO: {
USE_NOTIFICATION_SOUNDS: true,
USE_NOTIFICATION_SOUNDS: false,
SELECTED_NOTIFICATION_AUDIO_DEVICE: 'default',
NOTIFICATION_AUDIO_DEVICE: 0,
NOTIFICATION_SOUND: 0,
NOTIFICATION_VOLUME: 50,
SELECTED_TTS_AUDIO_DEVICE: 0,
TTS_AUDIO_DEVICE: 'default',
TTS_VOLUME: 50,
TTS_VOLUME: 50
},
THEME: {
USE_CUSTOM_THEME: false,
@ -173,21 +206,52 @@ async function createIniFile() {
MID_SECTION: '#6b8578',
CHAT_BUBBLE_BG: '#447466',
CHAT_BUBBLE_HEADER: '#ffffff',
CHAT_BUBBLE_MESSAGE: '#b5b5b5',
CHAT_BUBBLE_MESSAGE: '#b5b5b5'
},
TWITCH: {
USE_TWITCH: false,
SEND_CHAT: false,
CHANNEL_NAME: '',
CHANNEL_USER_ID: '',
USERNAME: '',
USER_ID: '',
USER_LOGO_URL: '',
OAUTH_TOKEN: '',
CLIENT_ID: '2t206sj7rvtr1rutob3p627d13jch9',
BETTERTTV_CHANNELS: [
{ value: 'turtlemaw', text: 'turtlemaw' },
{ value: 'tO_Ot', text: 'tO_Ot' },
{ value: 'adew', text: 'adew' }
],
CLIENT_ID: '2t206sj7rvtr1rutob3p627d13jch9'
},
TROVO: {
USE_TROVO: false,
SEND_CHAT: false,
CHANNEL_NAME: '',
CHANNEL_ID: '',
USERNAME: '',
USER_LOGO_URL: '',
USER_ID: '',
CLIENT_ID: '8d32385a4be4a29e345aedaf23ca772f',
OAUTH_TOKEN: ''
},
YOUTUBE: {
USE_YOUTUBE: false,
SEND_CHAT: false,
CHANNEL_ID: '',
CHANNEL_HANDLE: '',
LIVE_ID: ''
},
DLIVE: {
USE_DLIVE: false,
SEND_CHAT: false,
API_KEY: ''
},
MODULES: {
USE_MODULES: false,
USE_VTUBER: false,
USE_CHATBUBBLE: false,
USE_PNGTUBER: false,
USE_CHATBUBBLE: false
},
AMAZON: {
USE_AMAZON: false,
@ -195,16 +259,21 @@ async function createIniFile() {
ACCESS_SECRET: '',
PRIMARY_VOICE: '',
SECONDARY_VOICE: '',
CHARACTERS_USED: 0,
CHARACTERS_USED: 0
},
GOOGLE: {
USE_GOOGLE: false,
API_KEY: '',
PRIMARY_VOICE: '',
SECONDARY_VOICE: '',
CHARACTERS_USED: 0,
},
}).then(() => {
settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8'));
CHARACTERS_USED: 0
}
};
fs.writeFile(settingsPath, JSON.stringify(settingsx), error => {
if (error) {
console.error(error);
}
});
return settingsx;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<!doctype html>
<html lang="en">
<head>
<title>Chat</title>
<script
@ -10,14 +10,11 @@
<link rel="stylesheet" href="./fonts/FRAMCDN/font.css" />
<link href="main.css" rel="stylesheet" />
</head>
<body>
<!-- #region Main chat box-->
<div class="OptionPanel show" id="Chat">
<div id="chatBox" class="message-window">
<div class="texts"></div>
</div>
</div>
<script src="main.js"></script>
<video id="camera" autoplay></video>
<div id="chatBox" class="message-window"></div>
<emoji-picker class="dark"></emoji-picker>
</body>
<script src="main.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
</html>

View file

@ -1,171 +1,598 @@
body {
background-color: transparent;
@font-face {
font-family: 'FRAMDCN';
src: url(../fonts/FRAMCDN/FRAMDCN.woff);
}
:root {
--variable: 2s;
--buttonBackground: #bf2c2c;
overflow: hidden;
--main-color1: #6e2c8c;
--main-color1-temp: #6e2c8c;
/*Left bar and top right bar*/
--main-color2: white;
--main-color2-temp: white;
/*Icons and text*/
--main-color3: #211e1e;
--main-color3-temp: #211e1e;
/*Buttons and input*/
--main-color4: #2f2c34;
--main-color4-temp: #2f2c34;
--top-bar: #100b12;
--top-bar-temp: #100b12;
--mid-section: #352d3d;
--mid-section-temp: #352d3d;
--chat-bubble: #7a6d7f;
--chat-bubble-header: #141414;
--chat-bubble-username: white;
--chat-bubble-message: white;
--chat-bubble-temp: #7a6d7f;
--chat-bubble-header-temp: #141414;
--chat-bubble-message-temp: white;
}
.thomas {
position: relative;
float: center;
display: inline-block;
}
.speechbubble {
display: block;
bottom: 0;
position: absolute;
z-index: -1;
}
.fade-outx {
animation: fade-outx var(--variable) linear;
}
@keyframes fade-outx {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-outxx {
animation: fade-outxx var(--variable) linear;
}
@keyframes fade-outxx {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.bounce-in {
animation: bounce-in 1s ease;
}
@keyframes bounce-in {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
.bounce-inx {
animation: bounce-inx 1s ease;
}
@keyframes bounce-inx {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
}
.msg-container {
position: static;
display: inline-block;
width: 100%;
padding-top: 10px;
h1 {
font-family: 'FRAMDCN';
}
.message-window {
height: calc(100% - 50px);
height: calc(100% - 60px);
overflow: hidden;
overflow-y: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
width: 80%;
margin: auto;
background: transparent;
align-items: center;
flex-direction: column-reverse;
font-family: 'FRAMDCN';
position: relative;
z-index: 1;
}
.message-window::before {
content: '';
flex: 1 0 0px;
}
.OptionPanel {
flex: 3;
display: none;
position: absolute;
top: 10px;
left: 0;
.input-box {
display: flex;
border: none;
width: 100%;
height: calc(100% - 25px);
background: transparent;
height: 30px;
font-size: 16px;
}
.OptionPanel.show {
display: block;
.userText {
color: var(--chat-bubble-message);
font-family: Helvetica;
font-size: 16px;
text-align: right;
clear: both;
}
.message {
.userText span {
line-height: 1.5em;
display: inline-block;
background: #5ca6fa;
padding: 10px;
border-radius: 8px;
border-bottom-right-radius: 2px;
max-width: 80%;
margin-right: 10px;
animation: floatup 0.5s forwards;
}
.botText {
color: #000;
font-family: Helvetica;
font-weight: normal;
font-size: 16px;
text-align: left;
max-width: 100%;
height: auto;
min-width: 125px;
hyphens: auto;
}
.botText span {
line-height: 1.5em;
display: inline-block;
background: #e0e0e0;
padding: 10px;
border-radius: 8px;
border-bottom-left-radius: 2px;
max-width: 80%;
margin-left: 10px;
animation: floatup 0.5s forwards;
}
@keyframes floatup {
from {
transform: translateY(14px);
opacity: 0;
}
to {
transform: translateY(0px);
opacity: 1;
}
}
@media screen and (max-width: 600px) {
.full-chat-block {
width: 100%;
border-radius: 0px;
}
.chat-bar-collapsible {
position: fixed;
bottom: 0;
right: 0;
float: right;
overflow-wrap: break-word;
width: 100%;
}
.collapsible {
width: 100%;
border: 0px;
border-radius: 0px;
}
}
.message {
position: relative;
border: 2px solid #ff80e1;
box-shadow: 0 2px 10px rgba(255, 128, 225, 0.5);
background: linear-gradient(45deg, rgb(15, 12, 41, 0.7), rgb(48, 43, 99, 0.7));
/* background: linear-gradient(45deg, rgba(72, 0, 154, 0.7), rgba(138, 43, 226, 0.7)); */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-thumb {
background-color: #4c4c6a;
border-radius: 2px;
}
.chatBox {
width: 300px;
height: 400px;
max-height: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 4px var(--main-color4);
}
.chat-window {
flex: auto;
max-height: calc(100% - 60px);
background: #2f323b;
overflow: auto;
}
.chat-input {
height: 30px;
display: flex;
flex: 0 0 auto;
height: 60px;
background: var(--main-color3);
}
.chat-input input {
height: 59px;
line-height: 60px;
outline: 0 none;
border: none;
width: calc(100% - 60px);
color: var(--chat-bubble-message);
text-indent: 10px;
font-size: 12pt;
padding: 0;
background: var(--main-color3);
}
.chat-input button {
float: right;
outline: 0 none;
border: none;
background: var(--main-color3);
height: 40px;
width: 40px;
border-radius: 50%;
padding: 2px 0 0 0;
margin: 10px;
}
.chat-input input[good] + button {
box-shadow:
0 0 2px rgba(0, 0, 0, 0.12),
0 2px 4px rgba(0, 0, 0, 0.24);
}
.chat-input input[good] + button:hover {
box-shadow:
0 8px 17px 0 rgba(0, 0, 0, 0.2),
0 6px 20px 0 rgba(0, 0, 0, 0.19);
/* filter: brightness(150%); */
}
.chat-input input[good] + button path {
fill: var(--chat-bubble-message);
}
.msg-container {
direction: ltr;
position: static;
width: 100%;
padding: 10px 0px 0px 0px;
display: grid;
grid-template: 1fr / 1fr;
align-self: start;
}
.msg-container > * {
grid-column: 1 / 1;
grid-row: 1 / 1;
}
.msg-container.sender {
place-items: self-start;
}
.msg-container.user {
place-items: self-end;
}
.msg-box {
background: var(--chat-bubble);
color: white;
padding: 15px;
min-width: 100px;
border-radius: 5px;
padding: 18px 5px 5px 5px;
box-shadow:
0 0 2px rgba(0, 0, 0, 0.12),
0 2px 4px rgba(0, 0, 0, 0.24);
width: fit-content;
position: relative;
align-self: start;
}
.msg-box.sender {
margin: 25px 25px 0px 35px;
}
.msg-box.user {
text-align: left;
margin: 25px 35px 0px 0px;
}
.msg-box-user-temp {
background: var(--chat-bubble-temp);
}
.user-img {
display: inline-block;
position: relative;
border-radius: 50%;
height: 50px;
width: 50px;
z-index: 5;
align-self: start;
}
.messages.user {
margin-right: 20px;
}
.msg {
font-size: 12pt;
color: var(--chat-bubble-message);
margin: 0 0 0 0;
}
.msg-temp {
color: var(--chat-bubble-message-temp);
}
.timestamp {
color: var(--chat-bubble-header);
font-size: 10pt;
align-items: center;
font-family: 'xxii_avenmedium';
}
.timestamp-temp {
color: var(--chat-bubble-header-temp);
}
.username {
background-color: var(--main-color4);
color: white;
position: relative;
border-radius: 5px;
z-index: 3;
align-self: start;
}
.username.sender {
padding: 0px 5px 5px 30px;
margin: 20px 5px 5px 25px;
}
.username.user {
padding: 0px 30px 5px 5px;
margin: 20px 30px 5px 5px;
}
.username-temp {
color: var(--chat-bubble-header-temp);
}
.post-time {
font-size: 8pt;
color: white;
display: inline-block;
background-color: var(--main-color4);
position: relative;
z-index: 2;
border-radius: 5px;
align-self: start;
}
.post-time.sender {
padding: 5px 5px 5px 15px;
margin: 0px 0px 0px 50px;
}
.post-time.user {
padding: 5px 15px 5px 5px;
margin: 0px 50px 0px 0px;
}
.mmg {
display: flex;
}
.img {
height: 100%;
width: 100%;
border-radius: 50%;
}
.status-circle {
width: 20px;
border-radius: 50%;
z-index: 6;
position: relative;
align-self: start;
}
.status-circle.sender {
margin-left: 40px;
}
.status-circle.user {
margin-right: 40px;
}
.menu-select {
font-size: 0.9rem;
height: 40px;
border-radius: 20px;
background-color: var(--main-color3);
color: var(--main-color2);
align-items: center;
border: 0px;
padding-left: 10px;
width: 300px;
font-size: 100%;
padding: 10px;
padding-right: 25px;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='34' viewBox='0 0 24 24' width='32' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
background-repeat: no-repeat;
background-position-x: 100%;
background-position-y: 5px;
}
.top-select {
width: auto;
height: 24px;
padding: 0px;
margin: 0px;
background-color: transparent;
color: white;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
}
.info-image {
width: 50px;
height: 50px;
}
.top-select option {
margin: 40px;
background: rgba(0, 0, 0, 0.3);
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
background-color: var(--top-bar);
}
.AdvancedMenu {
border: 1px var(--main-color2) solid;
margin-top: 10px;
min-width: 555px;
border-radius: 5px;
border-radius: 5px;
}
.legendStyle {
margin-left: 1em;
padding: 0.2em 0.8em;
display: flex;
align-items: center;
}
.AdvancedMenuRow {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
justify-content: left;
margin-bottom: 10px;
}
.AdvancedMenuLabel {
font-size: 10pt;
padding-right: 5px;
margin-left: 10px;
width: 125px;
}
.AdvancedMenuLabel2 {
font-size: 10pt;
padding-right: 5px;
margin-left: 10px;
}
.AdvancedMenuLabel3 {
font-size: 12pt;
padding-right: 5px;
margin-left: 10px;
}
#SaveAdvancedSettingsButton {
margin-left: 10px;
}
.toggle {
position: relative;
display: inline-block;
width: 60px;
height: 40px;
background-color: var(--main-color3);
border-radius: 20px;
}
.message::after {
}
/* After slide changes */
.arrow {
.toggle:after {
content: '';
border: 2px solid #ff80e1;
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%) rotate(180deg);
border-width: 10px;
border-style: solid;
border-color: transparent transparent rgb(255, 128, 225, 0.7) transparent;
color: #ff80e1;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: var(--main-color2);
left: 5px;
top: 5px;
}
.sender {
color: #ff80e1;
font-size: 14pt;
/* Checkbox checked effect */
.checkbox:checked + .toggle::after {
left: 25px;
}
/* Checkbox checked toggle label bg color */
.checkbox:checked + .toggle {
background-color: var(--main-color1);
}
/* Checkbox vanished */
.checkbox {
display: none;
}
/* Small toggle */
/* toggle in label designing */
.toggle-small {
position: relative;
display: inline-block;
width: 30px;
height: 20px;
background-color: var(--main-color3);
border-radius: 10px;
margin-left: 10px;
}
/* After slide changes */
.toggle-small:after {
content: '';
position: absolute;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: white;
left: 2px;
top: 2px;
}
/* Checkbox checked effect */
.checkbox:checked + .toggle-small::after {
left: 13px;
}
/* Checkbox checked toggle label bg color */
.checkbox:checked + .toggle-small {
background-color: var(--main-color1);
}
.emotes {
position: relative;
cursor: pointer;
}
.dark {
display: none;
position: absolute;
z-index: 1;
top: -400px;
}
.emotes:hover .dark {
display: block;
}
.fi {
position: relative;
z-index: 5;
border-radius: 50%;
}
.translation-header {
background-color: var(--main-color4);
border-radius: 5px;
width: fit-content;
padding: 5px;
margin: 10px 0px 5px -5px;
position: relative;
}
.translation-message {
position: relative;
margin: 20px 0px 0px 0px;
}
.translation-message.user {
margin: -20px 0px 0px 0px;
}
.translation-icon {
position: relative;
padding: 0px 0px 0px 0px;
margin: -45px 0px 0px -40px;
top: 30px;
}
.language-icon {
position: relative;
top: 45px;
}
.flag-icon {
width: 20px !important;
height: 20px !important;
left: 18px;
}
.flag-icon.user {
left: -18px;
top: -15px;
}
.user-flag {
left: unset;
right: 18px;
top: -65px;
}

View file

@ -1,123 +1,89 @@
/* global io */
// Connect to the Socket.IO server
const socket = io();
const twitchTemplate = `
<img class="user-img" src="" />
<img class="status-circle sender" src="./images/twitch-icon.png" tip="twitch" />
<span class="post-time sender"></span>
<span class="username sender"></span>
<div class="msg-box sender"></div>
`.trim();
const emojiPicker = document.body.querySelector('emoji-picker');
const replaceChatMessageWithCustomEmojis = message =>
new Promise(resolve => {
const words = message.split(' ');
words.forEach(async word => {
if (word !== '') {
await emojiPicker.database.getEmojiByUnicodeOrName(word).then(data => {
if (data && data.name === word) {
const url = `<img src="${data.url}">`;
message = message.replace(word, url);
}
});
resolve(message);
}
});
});
// Emit a message to the server
socket.emit('message', 'Hello, Server!');
function getPostTime() {
const d = new Date();
document.body.querySelectorAll('.container').innerHTML = d.getHours();
const hours = d.getHours();
const minutes = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
const time = `${hours}:${minutes}`;
return time;
async function displayChatMessage(message) {
const article = document.createElement('article');
article.className = 'msg-container sender';
article.setAttribute('id', message.messageId);
article.innerHTML = twitchTemplate;
const userImg = article.querySelector('.user-img');
if (userImg) {
userImg.src = message.image;
}
const usernameHtml = article.querySelector('.username');
if (usernameHtml) {
usernameHtml.innerText = message.username;
}
const postTime = article.querySelector('.post-time');
if (postTime) {
postTime.innerText = message.postTime;
}
article.appendChild(postTime);
const formattedMessage = article.querySelector('.msg-box');
formattedMessage.innerHTML = message.message;
// if (formattedMessage) {
// messageObject.forEach(entry => {
// if (entry.text) {
// formattedMessage.innerHTML += entry.text;
// } else {
// formattedMessage.innerHTML += entry.html;
// }
// });
// }
await replaceChatMessageWithCustomEmojis(formattedMessage.innerHTML).then(data => {
formattedMessage.innerHTML = data;
showChatMessage(article);
});
}
function showChatMessage(article) {
const main = document.querySelector('#chatBox');
main.appendChild(article);
main.scrollTop = main.scrollHeight;
}
document.getElementById('chatBox').appendChild(article);
let textStreamContainer;
let x;
// const totalDuration = 5000; // Total duration in milliseconds
// const charactersPerSecond = 20; // Adjust the number of characters to display per second
const messages = document.body.querySelectorAll('.msg-container');
// const streamingSpeed = totalDuration / (textToStream.length / charactersPerSecond);
let currentIndex = 0;
let messageStream = '';
let tempMessageObject = '';
let fullMessageLength = 0;
function getFullMessageLength(text) {
let fullMessageLength = 0;
text.forEach((element) => {
if (element.text) {
fullMessageLength += element.text.length;
}
element.html;
fullMessageLength += 1;
});
return fullMessageLength;
}
function streamText() {
// if (currentIndex < fullMessageLength) {
// textStreamContainer.innerHTML += messageStream.filtered.charAt(currentIndex);
// currentIndex++;
// setTimeout(streamText, 50);
// }
if (currentIndex < messageStream.length) {
textStreamContainer.innerHTML += messageStream.charAt(currentIndex);
currentIndex++;
setTimeout(streamText, 50);
} else {
currentIndex = 0;
x.classList.add('fade-outx');
}
}
function displayTwitchMessage(logoUrl, username, messageObject) {
if (!messageObject) {
return;
}
const root = document.querySelector(':root');
root.style.setProperty('--variable', '5s');
const article = document.createElement('article');
x = article;
article.className = 'msg-container';
const placeMessage = `
<div class="thomas bounce-in">
<div class="message"></div>
<div class="sender"></div>
<div class="speechbubble"></div>
<div class="arrow"></div>
</div>
`.trim();
article.innerHTML = placeMessage;
const msg = article.querySelector('.message');
msg.innerHTML = `<div class="sender">${username}</div>`; //\n${message}`;
msg.style.fontSize = '12pt';
showChatMessage(article);
const elements = document.getElementsByClassName('msg-container');
if (elements.length > 1) {
elements[0].remove();
}
article.addEventListener('animationend', (e) => {
if (e.animationName == 'fade-outx') {
article.remove();
}
});
if (elements.length > 1) {
elements[0].classList.add('fade-outxx');
elements[0].addEventListener('animationend', (e) => {
if (e.animationName == 'fade-outxx') {
elements[0].remove();
}
});
}
// fullMessageLength = getFullMessageLength(messageObject);
messageStream = messageObject.filtered;
textStreamContainer = document.querySelector('.message');
streamText();
const lastMessage = messages[messages.length - 1];
lastMessage.scrollIntoView({ block: 'end', behavior: 'smooth' });
}
// // Receive a message from the server
socket.on('message', (logoUrl, username, message, messageDuration) => {
displayTwitchMessage(logoUrl, username, message);
socket.on('chat-in', message => {
displayChatMessage(message);
});

View file

@ -0,0 +1,14 @@
{
"name": "chat",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View file

@ -0,0 +1,10 @@
@font-face {
font-family: 'FRAMDCN';
src: local('FRAMDCN'), url('./FRAMDCN.woff') format('woff');
}
/* use this class to attach this font to any element i.e. <p class="fontsforweb_fontid_1381">Text with this font applied</p> */
.fontsforweb_fontid_1381 {
font-family: 'FRAMDCN' !important;
}
/* Font downloaded from FontsForWeb.com */

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title> web font from FontsForWeb.com</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="font.css">
<style type="text/css">
/* when @font-face is defined(it is in font.css) you can add the font to any rule by using font-family */
h1 {
font-family: 'FRAMDCN';
}
</style>
</head>
<body>
<h1>Thank you for using FontsForWeb.com</h1>
<p class="fontsforweb_fontid_1381">Look in the source of this file to see how to embed this font on your website</p>
</body>
</html>

View file

@ -0,0 +1,2 @@
Original download page
http://ttfonts.net/font/606_FranklinGothicMediumCond.htm

View file

@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<title>Chat</title>
<script
src="https://cdn.socket.io/4.6.0/socket.io.min.js"
integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="./fonts/FRAMCDN/font.css" />
<link href="main.css" rel="stylesheet" />
</head>
<body>
<!-- #region Main chat box-->
<div class="OptionPanel show" id="Chat">
<div id="chatBox" class="message-window">
<div class="texts"></div>
</div>
</div>
<script src="main.js"></script>
<video id="camera" autoplay></video>
</body>
</html>

View file

@ -0,0 +1,174 @@
body {
background-color: transparent;
font-family: 'FRAMDCN';
}
:root {
--variable: 2s;
--buttonBackground: #bf2c2c;
}
.thomas {
position: relative;
float: center;
/* display: inline-block; */
}
.speechbubble {
display: block;
bottom: 0;
position: absolute;
z-index: -1;
}
.fade-outx {
animation: fade-outx var(--variable) linear;
}
@keyframes fade-outx {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-outxx {
animation: fade-outxx var(--variable) linear;
}
@keyframes fade-outxx {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.bounce-in {
animation: bounce-in 1s ease;
}
@keyframes bounce-in {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
.bounce-inx {
animation: bounce-inx 1s ease;
}
@keyframes bounce-inx {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
}
.msg-container {
direction: ltr;
position: static;
width: 100%;
padding: 10px 0px 0px 0px;
display: grid;
grid-template: 1fr / 1fr;
align-self: center;
width: fit-content;
}
.msg-container > * {
grid-column: 1 / 1;
grid-row: 1 / 1;
}
.message-window {
height: calc(100% - 50px);
overflow: hidden;
overflow-y: hidden;
display: flex;
flex-direction: column;
width: 80%;
margin: auto;
background: transparent;
}
.message-window::before {
content: '';
flex: 1 0 0px;
}
.OptionPanel {
flex: 3;
display: none;
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: calc(100% - 25px);
background: transparent;
}
.OptionPanel.show {
display: block;
}
.message {
text-align: left;
max-width: 100%;
height: auto;
min-width: fit-content;
hyphens: auto;
/* bottom: 0; */
/* right: 0; */
/* float: right; */
overflow-wrap: break-word;
position: relative;
border: 2px solid #ff80e1;
/* box-shadow: 0 2px 10px rgba(255, 128, 225, 0.5); */
background: linear-gradient(45deg, rgb(15, 12, 41, 0.7), rgb(48, 43, 99, 0.7));
/* background: linear-gradient(45deg, rgba(72, 0, 154, 0.7), rgba(138, 43, 226, 0.7)); */
color: white;
padding: 15px;
border-radius: 20px;
margin-bottom: 10px;
}
.arrow {
content: '';
border: 2px solid #ff80e1;
position: absolute;
left: 50%;
transform: translateX(-50%) rotate(180deg);
border-width: 10px;
border-style: solid;
border-color: transparent transparent rgb(255, 128, 225, 0.7) transparent;
color: #ff80e1;
bottom: -10px;
}
.sender {
color: #ff80e1;
font-size: 14pt;
}

View file

@ -0,0 +1,125 @@
/* global io */
// Connect to the Socket.IO server
const socket = io();
// Emit a message to the server
socket.emit('message', 'Hello, Server!');
function getPostTime() {
const d = new Date();
document.body.querySelectorAll('.container').innerHTML = d.getHours();
const hours = d.getHours();
const minutes = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
const time = `${hours}:${minutes}`;
return time;
}
function showChatMessage(article) {
const main = document.querySelector('#chatBox');
main.appendChild(article);
main.scrollTop = main.scrollHeight;
}
let textStreamContainer;
let x;
// const totalDuration = 5000; // Total duration in milliseconds
// const charactersPerSecond = 20; // Adjust the number of characters to display per second
// const streamingSpeed = totalDuration / (textToStream.length / charactersPerSecond);
let currentIndex = 0;
let messageStream = '';
const tempMessageObject = '';
const fullMessageLength = 0;
function getFullMessageLength(text) {
let fullMessageLength = 0;
text.forEach(element => {
if (element.text) {
fullMessageLength += element.text.length;
}
// element.html;
fullMessageLength += 1;
});
return fullMessageLength;
}
function streamText() {
// if (currentIndex < fullMessageLength) {
// textStreamContainer.innerHTML += messageStream.filtered.charAt(currentIndex);
// currentIndex++;
// setTimeout(streamText, 50);
// }
if (currentIndex < messageStream.length) {
textStreamContainer.innerHTML += messageStream.charAt(currentIndex);
currentIndex++;
setTimeout(streamText, 50);
} else {
currentIndex = 0;
x.classList.add('fade-outx');
}
}
function displayTwitchMessage(message) {
if (!message.filteredMessage) {
return;
}
const root = document.querySelector(':root');
root.style.setProperty('--variable', '5s');
const article = document.createElement('article');
x = article;
article.className = 'msg-container';
const placeMessage = `
<div class="thomas bounce-in">
<div class="message"></div>
<div class="sender"></div>
<div class="speechbubble"></div>
<div class="arrow"></div>
</div>
`.trim();
article.innerHTML = placeMessage;
const msg = article.querySelector('.message');
msg.innerHTML = `<div class="sender">${message.username}</div>`; // \n${message}`;
msg.style.fontSize = '12pt';
showChatMessage(article);
const elements = document.getElementsByClassName('msg-container');
if (elements.length > 1) {
elements[0].remove();
}
article.addEventListener('animationend', e => {
if (e.animationName === 'fade-outx') {
article.remove();
}
});
if (elements.length > 1) {
elements[0].classList.add('fade-outxx');
elements[0].addEventListener('animationend', e => {
if (e.animationName === 'fade-outxx') {
elements[0].remove();
}
});
}
// fullMessageLength = getFullMessageLength(messageObject);
messageStream = message.filteredMessage;
textStreamContainer = document.querySelector('.message');
streamText();
}
// // Receive a message from the server
socket.on('message', message => {
displayTwitchMessage(message);
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,478 @@
const { FaceLandmarker, HandLandmarker, PoseLandmarker, FilesetResolver, DrawingUtils } = require('@mediapipe/tasks-vision');
const videoBlendShapes = document.getElementById('video-blend-shapes');
const videoSelect = document.querySelector('select#videoSource');
const videoElement = document.getElementById('video');
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');
let cameraRunning = false;
let cameraVisible = true;
let cameraFPS = 30;
let cameraWidth = 640;
let cameraHeight = 480;
let lastVideoTime = -1;
const drawingUtils = new DrawingUtils(canvasCtx);
getDevices().then(gotDevices);
let detections = [];
document.body.querySelector('#cameraVisible').addEventListener('click', async () => {
cameraVisible = !cameraVisible;
if (cameraVisible) {
document.body.querySelector('#cameraVisible').innerHTML = 'Hide Camera';
document.body.querySelector('#video').style.display = '';
} else {
document.body.querySelector('#cameraVisible').innerHTML = 'Show Camera';
document.body.querySelector('#video').style.display = 'none';
}
});
document.body.querySelector('#cameraRunning').addEventListener('click', async () => {
cameraRunning = !cameraRunning;
if (cameraRunning) {
getStream();
document.body.querySelector('#cameraRunning').innerHTML = 'Stop Camera';
} else {
if (window.stream) {
window.stream.getTracks().forEach(track => {
track.stop();
});
}
document.body.querySelector('#cameraRunning').innerHTML = 'Start Camera';
}
});
document.body.querySelector('#cameraResolution').addEventListener('change', async e => {
cameraWidth = e.target.options[e.target.selectedIndex].getAttribute('width');
cameraHeight = e.target.options[e.target.selectedIndex].getAttribute('height');
});
document.body.querySelector('#cameraFPS').addEventListener('change', async () => {
cameraFPS = document.body.querySelector('#cameraFPS').value;
});
// -------------- Face Detection --------------
let faceDetectionDelegate = 'GPU';
let minFaceDetectionConfidence = 0.5;
document.getElementById('minFaceDetectionConfidenceValue').innerHTML = minFaceDetectionConfidence;
let minFacePresenceConfidence = 0.5;
document.getElementById('minFacePresenceConfidenceValue').innerHTML = minFacePresenceConfidence;
let minFaceTrackingConfidence = 0.5;
document.getElementById('minFaceTrackingConfidenceValue').innerHTML = minFaceTrackingConfidence;
let faceTrackingEnabled = false;
let hideFace = false;
let faceLandmarker;
let resultsFace;
document.body.querySelector('#face').addEventListener('click', async () => {
faceTrackingEnabled = !faceTrackingEnabled;
if (faceTrackingEnabled) {
createFaceLandmarker();
document.body.querySelector('#face').innerHTML = 'Disable face detection';
} else {
faceLandmarker = null;
document.body.querySelector('#face').innerHTML = 'Enable face detection';
}
});
document.body.querySelector('#hideFace').addEventListener('click', async () => {
hideFace = !hideFace;
if (hideFace) {
document.body.querySelector('#hideFace').innerHTML = 'Show face detection';
} else {
document.body.querySelector('#hideFace').innerHTML = 'Hide face detection';
}
});
document.body.querySelector('#faceDetectionDelegate').addEventListener('change', async () => {
faceDetectionDelegate = document.getElementById('faceDetectionDelegate').value;
if (faceTrackingEnabled) {
createFaceLandmarker();
}
});
document.body.querySelector('#minFaceDetectionConfidence').addEventListener('change', async () => {
minFaceDetectionConfidence = parseInt(document.getElementById('minFaceDetectionConfidence').value);
document.getElementById('minFaceDetectionConfidenceValue').innerHTML = minFaceDetectionConfidence;
if (faceTrackingEnabled) {
createFaceLandmarker();
}
});
document.body.querySelector('#minFacePresenceConfidence').addEventListener('change', async () => {
minFacePresenceConfidence = parseInt(document.getElementById('minFacePresenceConfidence').value);
document.getElementById('minFacePresenceConfidenceValue').innerHTML = minFacePresenceConfidence;
if (faceTrackingEnabled) {
createFaceLandmarker();
}
});
document.body.querySelector('#minFaceTrackingConfidence').addEventListener('change', async () => {
minFaceTrackingConfidence = parseInt(document.getElementById('minFaceTrackingConfidence').value);
document.getElementById('minFaceTrackingConfidenceValue').innerHTML = minFaceTrackingConfidence;
if (faceTrackingEnabled) {
createFaceLandmarker();
}
});
// -------------- Hand Detection --------------
let handDetectionDelegate = 'GPU';
let minHandDetectionConfidence = 0.5;
document.getElementById('minHandDetectionConfidenceValue').innerHTML = minHandDetectionConfidence;
let minHandPresenceConfidence = 0.5;
document.getElementById('minHandPresenceConfidenceValue').innerHTML = minHandPresenceConfidence;
let minHandTrackingConfidence = 0.5;
document.getElementById('minHandTrackingConfidenceValue').innerHTML = minHandTrackingConfidence;
let handTrackingEnabled = false;
let hideHand = false;
let handLandmarker;
let resultsHands;
document.body.querySelector('#hand').addEventListener('click', async () => {
handTrackingEnabled = !handTrackingEnabled;
if (handTrackingEnabled) {
createHandLandmarker();
document.body.querySelector('#hand').innerHTML = 'Disable hand detection';
} else {
handLandmarker = null;
document.body.querySelector('#hand').innerHTML = 'Enable hand detection';
}
});
document.body.querySelector('#hideHand').addEventListener('click', async () => {
hideHand = !hideHand;
if (hideFace) {
document.body.querySelector('#hideHand').innerHTML = 'Show hand detection';
} else {
document.body.querySelector('#hideHand').innerHTML = 'Hide hand detection';
}
});
document.body.querySelector('#handDetectionDelegate').addEventListener('change', async () => {
handDetectionDelegate = document.getElementById('handDetectionDelegate').value;
if (handTrackingEnabled) {
createHandLandmarker();
}
});
document.body.querySelector('#minHandDetectionConfidence').addEventListener('change', async () => {
minHandDetectionConfidence = parseInt(document.getElementById('minHandDetectionConfidence').value);
document.getElementById('minHandDetectionConfidenceValue').innerHTML = minHandDetectionConfidence;
if (handTrackingEnabled) {
createHandLandmarker();
}
});
document.body.querySelector('#minHandPresenceConfidence').addEventListener('change', async () => {
minHandPresenceConfidence = parseInt(document.getElementById('minHandPresenceConfidence').value);
document.getElementById('minHandPresenceConfidenceValue').innerHTML = minHandPresenceConfidence;
if (handTrackingEnabled) {
createHandLandmarker();
}
});
document.body.querySelector('#minHandTrackingConfidence').addEventListener('change', async () => {
minHandTrackingConfidence = parseInt(document.getElementById('minHandTrackingConfidence').value);
document.getElementById('minHandTrackingConfidenceValue').innerHTML = minHandTrackingConfidence;
if (handTrackingEnabled) {
createHandLandmarker();
}
});
// -------------- Pose Detection --------------
let poseDetectionDelegate = 'GPU';
let minPoseDetectionConfidence = 0.5;
document.getElementById('minPoseDetectionConfidenceValue').innerHTML = minPoseDetectionConfidence;
let minPosePresenceConfidence = 0.5;
document.getElementById('minPosePresenceConfidenceValue').innerHTML = minPosePresenceConfidence;
let minPoseTrackingConfidence = 0.5;
document.getElementById('minPoseTrackingConfidenceValue').innerHTML = minPoseTrackingConfidence;
let poseTrackingEnabled = false;
let hidepose = false;
let poseLandmarker;
let resultsPose;
document.body.querySelector('#pose').addEventListener('click', async () => {
poseTrackingEnabled = !poseTrackingEnabled;
if (poseTrackingEnabled) {
createPoseLandmarker();
document.body.querySelector('#pose').innerHTML = 'Disable pose detection';
} else {
poseLandmarker = null;
document.body.querySelector('#pose').innerHTML = 'Enable pose detection';
}
});
document.body.querySelector('#hidePose').addEventListener('click', async () => {
hidepose = !hidepose;
if (hideFace) {
document.body.querySelector('#hidePose').innerHTML = 'Show pose detection';
} else {
document.body.querySelector('#hidePose').innerHTML = 'Hide pose detection';
}
});
document.body.querySelector('#poseDetectionDelegate').addEventListener('change', async () => {
poseDetectionDelegate = document.getElementById('faceDetectionDelegate').value;
if (poseTrackingEnabled) {
createPoseLandmarker();
}
});
document.body.querySelector('#minPoseDetectionConfidence').addEventListener('change', async () => {
minPoseDetectionConfidence = parseInt(document.getElementById('minPoseDetectionConfidence').value);
document.getElementById('minPoseDetectionConfidenceValue').innerHTML = minPoseDetectionConfidence;
if (poseTrackingEnabled) {
createPoseLandmarker();
}
});
document.body.querySelector('#minPosePresenceConfidence').addEventListener('change', async () => {
minPosePresenceConfidence = parseInt(document.getElementById('minPosePresenceConfidence').value);
document.getElementById('minPosePresenceConfidenceValue').innerHTML = minPosePresenceConfidence;
if (poseTrackingEnabled) {
createPoseLandmarker();
}
});
document.body.querySelector('#minPoseTrackingConfidence').addEventListener('change', async () => {
minPoseTrackingConfidence = parseInt(document.getElementById('minPoseTrackingConfidence').value);
document.getElementById('minPoseTrackingConfidenceValue').innerHTML = minPoseTrackingConfidence;
if (poseTrackingEnabled) {
createPoseLandmarker();
}
});
// -------------- Camera --------------
function getDevices() {
// AFAICT in Safari this only gets default devices until gUM is called :/
return navigator.mediaDevices.enumerateDevices();
}
function gotDevices(deviceInfos) {
window.deviceInfos = deviceInfos; // make available to console
for (const deviceInfo of deviceInfos) {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `Camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
}
}
}
function getStream() {
if (window.stream) {
window.stream.getTracks().forEach(track => {
track.stop();
});
}
const videoSource = videoSelect.value;
const constraints = {
video: {
deviceId: videoSource ? { exact: videoSource } : undefined,
frameRate: { min: 30, ideal: cameraFPS, max: 60 },
width: cameraWidth,
height: cameraHeight
}
};
return navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError);
}
function gotStream(stream) {
window.stream = stream; // make stream available to console
videoSelect.selectedIndex = [...videoSelect.options].findIndex(option => option.text === stream.getVideoTracks()[0].label);
videoElement.srcObject = stream;
videoElement.addEventListener('loadeddata', predictWebcam);
}
function handleError(error) {
console.error('Error: ', error);
}
// -------------- Tracking --------------
async function createFaceLandmarker() {
const filesetResolver = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm');
faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
baseOptions: {
modelAssetPath: 'face_landmarker_model/face_landmarker.task',
delegate: faceDetectionDelegate
},
minFaceDetectionConfidence: minFaceDetectionConfidence,
minFacePresenceConfidence: minFacePresenceConfidence,
minTrackingConfidence: minFaceTrackingConfidence,
outputFaceBlendshapes: true,
runningMode: 'VIDEO',
numFaces: 1
});
window.requestAnimationFrame(predictWebcam);
}
const createHandLandmarker = async () => {
const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm');
handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: 'hand_landmarker_model/hand_landmarker.task',
delegate: handDetectionDelegate
},
minHandDetectionConfidence: minHandDetectionConfidence,
minHandPresenceConfidence: minHandPresenceConfidence,
minTrackingConfidence: minHandTrackingConfidence,
runningMode: 'VIDEO',
numHands: 2
});
window.requestAnimationFrame(predictWebcam);
};
const createPoseLandmarker = async () => {
const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm');
poseLandmarker = await PoseLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: 'pose_landmarker_model/pose_landmarker_heavy.task',
delegate: poseDetectionDelegate
},
minPoseDetectionConfidence: minPoseDetectionConfidence,
minPosePresenceConfidence: minPosePresenceConfidence,
minTrackingConfidence: minPoseTrackingConfidence,
runningMode: 'VIDEO',
numPoses: 1
});
window.requestAnimationFrame(predictWebcam);
};
// -------------- Detection --------------
function mediapipeRunning() {
if (faceLandmarker || handLandmarker || poseLandmarker) {
return true;
}
return false;
}
function predictWebcam() {
if (!mediapipeRunning()) {
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
return;
}
if (!hideFace || !hidepose || !hideHand) {
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
} else {
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
}
// Now let's start detecting the stream.
const startTimeMs = performance.now();
if (lastVideoTime !== videoElement.currentTime) {
lastVideoTime = videoElement.currentTime;
resultsFace = faceLandmarker ? faceLandmarker.detectForVideo(videoElement, startTimeMs) : null;
// resultsHands = handLandmarker ? handLandmarker.detectForVideo(videoElement, startTimeMs) : null;
// resultsPose = poseLandmarker ? poseLandmarker.detectForVideo(videoElement, startTimeMs) : null;
}
if (faceTrackingEnabled && resultsFace && resultsFace.faceLandmarks) {
// console.log(resultsFace);
drawDebugFace(resultsFace);
}
if (handTrackingEnabled && resultsHands && resultsHands.landmarks) {
// drawDebugHands(resultsHands);
}
if (poseTrackingEnabled && resultsPose && resultsPose.landmarks) {
// drawDebugPose(resultsPose);
}
// drawBlendShapes(videoBlendShapes, resultsFace.faceBlendshapes);
// canvasCtx.restore();
// Call this function again to keep predicting when the browser is ready.
if (cameraRunning === true) {
window.requestAnimationFrame(predictWebcam);
}
}
function drawDebugFace(results) {
if (results) {
detections = results;
}
if (hideFace) {
return;
}
for (const landmarks of results.faceLandmarks) {
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_TESSELATION, { color: '#C0C0C070', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE, { color: '#FF3030', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYEBROW, { color: '#FF3030', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYE, { color: '#30FF30', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYEBROW, { color: '#30FF30', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_FACE_OVAL, { color: '#E0E0E0', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LIPS, { color: '#E0E0E0', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS, { color: '#FF3030', lineWidth: 1 });
drawingUtils.drawConnectors(landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS, { color: '#30FF30', lineWidth: 1 });
}
}
function drawDebugHands(results) {
if (hideHand) {
return;
}
for (const landmarks of results.landmarks) {
drawingUtils.drawConnectors(landmarks, HandLandmarker.HAND_CONNECTIONS, {
color: '#E0E0E0',
lineWidth: 1
});
drawingUtils.drawLandmarks(landmarks, { color: '#E0E0E0', lineWidth: 1 });
}
}
function drawDebugPose(results) {
if (hidePose) {
return;
}
for (const landmark of results.landmarks) {
drawingUtils.drawLandmarks(landmark, {
radius: data => DrawingUtils.lerp(data.from.z, -0.15, 0.1, 5, 1)
});
drawingUtils.drawConnectors(landmark, PoseLandmarker.POSE_CONNECTIONS, { color: '#E0E0E0', lineWidth: 1 });
}
}
function drawBlendShapes(el, blendShapes) {
if (!blendShapes.length) {
return;
}
// console.log(blendShapes[0]);
let htmlMaker = '';
blendShapes[0].categories.map(shape => {
htmlMaker += `
<li class="blend-shapes-item">
<span class="blend-shapes-label">${shape.displayName || shape.categoryName}</span>
<span class="blend-shapes-value" style="width: calc(${+shape.score * 100}% - 120px)">${(+shape.score).toFixed(4)}</span>
</li>
`;
});
el.innerHTML = htmlMaker;
}

View file

@ -0,0 +1,75 @@
{
"shapes": [
{
"fill_H": 0,
"fill_S": 0,
"fill_B": 15,
"fill_O": 100,
"stroke_H": 263.0718090188531,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
152,
148,
176,
149,
150,
136,
172,
58,
132,
93,
234,
127,
162,
21,
54,
103,
67,
109,
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152
]
},
{
"fill_H": 116.38012093685805,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 234.7247860532549,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
151,
50,
280,
151,
9,
330,
101,
9,
151,
361
]
}
]
}

View file

@ -0,0 +1,448 @@
{
"shapes": [
{
"fill_H": 230,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 25,
"stroke_S": 0,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
14,
87,
178,
88,
95,
78,
191,
80,
81,
82,
13,
12,
11,
0,
164,
2,
94,
19,
1,
4,
5,
195,
197,
6,
122,
245,
244,
243,
112,
26,
22,
23,
24,
110,
226,
247,
30,
29,
27,
28,
56,
190,
243,
244,
245,
122,
6,
168,
8,
9,
151,
10,
109,
67,
103,
54,
21,
162,
127,
234,
93,
132,
58,
172,
136,
150,
149,
176,
148,
152,
175,
199,
200,
18,
17,
16,
15,
14
]
},
{
"fill_H": 75,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 0,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
152,
175,
199,
200,
18,
17,
16,
15,
14,
317,
402,
318,
324,
308,
415,
310,
311,
312,
13,
12,
11,
0,
164,
2,
94,
19,
1,
4,
5,
195,
197,
6,
351,
465,
464,
463,
341,
256,
252,
253,
254,
339,
446,
467,
260,
259,
257,
258,
286,
414,
463,
464,
465,
351,
6,
168,
8,
9,
151,
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152,
356
]
},
{
"fill_H": 115,
"fill_S": 50,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
27,
28,
56,
190,
243,
112,
26,
22,
23,
24,
110,
226,
247,
30,
29,
27,
323
]
},
{
"fill_H": 115,
"fill_S": 50,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
257,
259,
260,
467,
446,
339,
254,
253,
252,
256,
341,
463,
414,
286,
258,
257
]
},
{
"fill_H": 355,
"fill_S": 70,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 0,
"indices": [
1,
44,
220,
134,
51,
5,
281,
363,
440,
274,
1
]
},
{
"fill_H": 355,
"fill_S": 0,
"fill_B": 30,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 0,
"indices": [
27,
52,
29,
27
]
},
{
"fill_H": 355,
"fill_S": 0,
"fill_B": 30,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 0,
"indices": [
23,
101,
24,
23
]
},
{
"fill_H": 320,
"fill_S": 0,
"fill_B": 30,
"fill_O": 100,
"stroke_H": 235,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
257,
282,
259,
257
]
},
{
"fill_H": 320,
"fill_S": 0,
"fill_B": 30,
"fill_O": 100,
"stroke_H": 235,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
253,
330,
254,
253
]
},
{
"fill_H": 210,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 170,
"stroke_S": 0,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
61,
76,
62,
78,
95,
88,
178,
87,
14,
317,
402,
318,
324,
308,
292,
306,
291,
306,
292,
308,
319,
404,
315,
16,
85,
180,
89,
78,
62,
76,
61
]
},
{
"fill_H": 5,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 50,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
78,
191,
80,
81,
82,
13,
312,
311,
310,
415,
308,
303,
302,
12,
72,
73,
183,
78,
264
]
},
{
"fill_H": 135,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 40,
"stroke_S": 0,
"stroke_B": 70,
"stroke_O": 100,
"indices": [
107,
66,
105,
63,
70
]
},
{
"fill_H": 135,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 40,
"stroke_S": 0,
"stroke_B": 70,
"stroke_O": 100,
"indices": [
336,
296,
334,
293,
300
]
},
{
"fill_H": 135,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 40,
"stroke_S": 0,
"stroke_B": 70,
"stroke_O": 100,
"indices": []
}
]
}

View file

@ -0,0 +1,325 @@
{
"shapes": [
{
"fill_H": 0,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 0,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
2,
167,
92,
216,
207,
187,
147,
137,
234,
127,
162,
21,
54,
103,
67,
109,
10,
151,
9,
8,
168,
6,
197,
195,
5,
3,
196,
188,
233,
243,
190,
56,
28,
27,
225,
130,
25,
110,
24,
23,
22,
233,
188,
196,
3,
5,
4,
1,
19,
94,
2
]
},
{
"fill_H": 0,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 0,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
2,
393,
322,
436,
427,
411,
376,
366,
454,
356,
389,
251,
284,
332,
297,
338,
10,
151,
9,
8,
168,
6,
197,
195,
5,
248,
419,
412,
453,
463,
414,
286,
258,
257,
445,
359,
255,
339,
254,
253,
252,
453,
412,
419,
248,
5,
4,
1,
19,
94,
2
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 130,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
188,
245,
189,
221,
222,
223,
46,
226,
31,
228,
229,
230,
231,
128,
233,
22,
23,
24,
110,
25,
130,
225,
27,
28,
56,
190,
243,
233,
128,
188
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
412,
465,
413,
441,
442,
276,
446,
261,
448,
449,
450,
451,
357,
453,
252,
253,
254,
339,
255,
359,
445,
257,
258,
286,
414,
463,
453,
357,
412
]
},
{
"fill_H": 70,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 0,
"stroke_S": 0,
"stroke_B": 30,
"stroke_O": 100,
"indices": [
19,
1,
45,
1,
275
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
355,
371,
280,
352
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
277,
329,
347,
346,
340
]
},
{
"fill_H": 225,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
47,
100,
118,
117,
111
]
},
{
"fill_H": 225,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
126,
142,
50,
123
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
168,
107,
151,
336,
168,
447
]
},
{
"fill_H": 0,
"fill_S": 100,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 0,
"stroke_S": 100,
"stroke_B": 100,
"stroke_O": 0,
"indices": []
}
]
}

View file

@ -0,0 +1,167 @@
{
"shapes": [
{
"fill_H": 225,
"fill_S": 5,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 255,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
152,
148,
176,
149,
150,
136,
172,
58,
132,
93,
234,
127,
162,
21,
54,
103,
67,
109,
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152
]
},
{
"fill_H": 360,
"fill_S": 45,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 255,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
73,
180,
84,
17,
314,
404,
303,
312,
82,
73,
288
]
},
{
"fill_H": 360,
"fill_S": 45,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 360,
"stroke_S": 60,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
13,
17
]
},
{
"fill_H": 360,
"fill_S": 45,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 265,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
92,
73,
82,
13,
312,
303,
322
]
},
{
"fill_H": 345,
"fill_S": 50,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 130,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
225,
100
]
},
{
"fill_H": 345,
"fill_S": 50,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 130,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
221,
117
]
},
{
"fill_H": 345,
"fill_S": 50,
"fill_B": 100,
"fill_O": 0,
"stroke_H": 130,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
453,
452,
451,
450,
449,
448,
261
]
},
{
"fill_H": 360,
"fill_S": 45,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 265,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": []
}
]
}

View file

@ -0,0 +1,160 @@
{
"shapes": [
{
"fill_H": 160,
"fill_S": 25,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 80,
"stroke_O": 100,
"indices": [
152,
170,
138,
177,
93,
234,
127,
162,
21,
54,
103,
67,
109,
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
401,
367,
395,
152
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 80,
"stroke_O": 100,
"indices": [
142,
101,
118,
31,
226,
113,
225,
224,
223,
222,
221,
189,
245,
217,
209,
142
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 80,
"stroke_O": 100,
"indices": [
429,
437,
465,
413,
441,
442,
443,
444,
445,
342,
446,
261,
347,
330,
371,
429
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 80,
"stroke_O": 100,
"indices": [
89,
81,
38,
12,
268,
311,
319,
89
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 25,
"stroke_O": 100,
"indices": [
60,
60,
97
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 25,
"stroke_O": 100,
"indices": [
290,
328,
326
]
},
{
"fill_H": 160,
"fill_S": 95,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 160,
"stroke_S": 50,
"stroke_B": 25,
"stroke_O": 100,
"indices": []
}
]
}

View file

@ -0,0 +1,115 @@
{
"shapes": [
{
"fill_H": 270,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 285,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
8,
8,
8,
55,
65,
52,
53,
70,
71,
21,
139,
143,
111,
117,
118,
126,
198,
236,
3,
195,
196,
188,
233,
23,
24,
110,
25,
130,
113,
29,
27,
28,
56,
190,
243,
233,
188,
196,
195
]
},
{
"fill_H": 270,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 285,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": [
8,
285,
295,
282,
283,
300,
301,
251,
368,
372,
340,
346,
347,
355,
420,
456,
248,
195,
419,
412,
453,
253,
254,
339,
255,
359,
342,
259,
257,
258,
286,
414,
463,
453,
412,
419,
195
]
},
{
"fill_H": 270,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 285,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 100,
"indices": []
}
]
}

View file

@ -0,0 +1,188 @@
{
"shapes": [
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
150,
150,
212,
216,
206,
98,
97,
2,
462,
250,
459,
440,
363,
456,
399,
412,
465,
413,
441,
442,
443,
444,
445,
342,
265,
372,
264,
389,
251,
284,
332,
297,
338,
10,
109,
67,
103,
54,
21,
162,
127,
34,
143,
35,
226,
113,
225,
224,
223,
222,
221,
189,
244,
233,
232,
231,
230,
229,
228,
31,
226,
35,
143,
34,
127,
234,
93,
132,
58,
172,
136,
150
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152,
148,
176,
149,
150,
212,
216,
206,
98,
97,
2,
164,
0,
11,
12,
13,
82,
81,
80,
191,
78,
95,
88,
178,
87,
14,
317,
402,
318,
324,
308,
415,
310,
311,
312,
13,
12,
11,
0,
164,
2,
250,
459,
440,
363,
456,
399,
412,
465,
413,
464,
453,
452,
451,
450,
449,
448,
261,
446,
342,
265,
372,
264,
389,
397
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": []
}
]
}

View file

@ -0,0 +1,206 @@
{
"shapes": [
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
4,
45,
218,
235,
203,
205,
123,
227,
162,
21,
54,
104,
69,
108,
151,
9,
8,
168,
6,
122,
245,
244,
243,
190,
56,
28,
27,
225,
130,
25,
110,
24,
23,
22,
26,
112,
243,
244,
245,
122,
6,
197,
195,
5,
4
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
130,
46,
223,
222,
221,
189,
245,
188,
174,
114,
121,
231,
24,
23,
22,
26,
112,
243,
190,
56,
28,
27,
225,
247,
25,
130,
397
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
4,
275,
438,
455,
423,
425,
352,
447,
389,
251,
284,
333,
299,
337,
151,
9,
8,
168,
6,
351,
465,
464,
463,
414,
286,
258,
257,
445,
359,
255,
339,
254,
253,
252,
256,
341,
463,
464,
465,
351,
6,
197,
195,
5,
4
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
399,
412,
465,
413,
441,
442,
443,
276,
342,
255,
467,
445,
257,
258,
286,
414,
463,
341,
256,
252,
253,
254,
451,
350,
343,
399
]
},
{
"fill_H": 155,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 330,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": []
}
]
}

View file

@ -0,0 +1,159 @@
{
"shapes": [
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 90,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152,
148,
176,
149,
150,
136,
172,
58,
132,
93,
234,
127,
162,
21,
54,
103,
67,
109,
10
]
},
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
6,
6,
294,
64,
6
]
},
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
31,
65,
233,
31
]
},
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
465,
258,
265,
253,
465
]
},
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
0,
312,
302,
311,
303,
310,
270,
409,
291,
375,
320,
324,
404,
402,
315,
14,
85,
178,
180,
88,
91,
95,
146,
61,
185,
40,
42,
39,
81,
37,
82
]
},
{
"fill_H": 40,
"fill_S": 100,
"fill_B": 0,
"fill_O": 100,
"stroke_H": 30,
"stroke_S": 100,
"stroke_B": 50,
"stroke_O": 100,
"indices": [
397
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

View file

@ -0,0 +1,123 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div class="select">
<div>
<h3>Camera</h3>
<label for="videoSource">Camera:</label
><select id="videoSource">
<option value="">None</option>
</select>
</div>
<div>
<label for="fname">W x H</label>
<select id="cameraResolution">
<option value="0" width="640" height="480">640 x 480 (4:3)</option>
<option value="1" width="1920" height="1080">1920 x 1080 (16:9)</option>
<option value="2" width="1280" height="720">1280 x 720 (16:9)</option>
<option value="3" width="320" height="240">320 x 240 (4:3)</option>
</select>
<label for="fname">FPS</label>
<select id="cameraFPS">
<option value="30">30</option>
<option value="60">60</option>
</select>
</div>
<div>
<button id="cameraRunning">Start camera</button>
<button id="cameraVisible">Hide Camera</button>
</div>
<div>
<h3>Face</h3>
<label>Face Inference delegate</label>
<select id="faceDetectionDelegate">
<option value="GPU">GPU</option>
<option value="CPU">CPU</option>
</select>
<div class="slidecontainer">
<label>Minimum face detection confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minFaceDetectionConfidence" />
<span id="minFaceDetectionConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum face presence confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minFacePresenceConfidence" />
<span id="minFacePresenceConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum face tracking confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minFaceTrackingConfidence" />
<span id="minFaceTrackingConfidenceValue"></span>
</div>
<button id="face">Enable face detection</button>
<button id="hideFace">Hide face detection</button>
</div>
<div>
<h3>Hands</h3>
<label>Hands Inference delegate</label>
<select id="handDetectionDelegate">
<option value="GPU">GPU</option>
<option value="CPU">CPU</option>
</select>
<div class="slidecontainer">
<label>Minimum hand detection confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minHandDetectionConfidence" />
<span id="minHandDetectionConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum hand presence confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minHandPresenceConfidence" />
<span id="minHandPresenceConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum hand tracking confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minHandTrackingConfidence" />
<span id="minHandTrackingConfidenceValue"></span>
</div>
<button id="hand">Enable hand detection</button>
<button id="hideHand">Hide hand detection</button>
</div>
<div>
<h3>Pose</h3>
<label>Pose Inference delegate</label>
<select id="poseDetectionDelegate">
<option value="GPU">GPU</option>
<option value="CPU">CPU</option>
</select>
<div class="slidecontainer">
<label>Minimum pose detection confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minPoseDetectionConfidence" />
<span id="minPoseDetectionConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum pose presence confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minPosePresenceConfidence" />
<span id="minPosePresenceConfidenceValue"></span>
</div>
<div class="slidecontainer">
<label>Minimum pose tracking confidence</label>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="slider" id="minPoseTrackingConfidence" />
<span id="minPoseTrackingConfidenceValue"></span>
</div>
<button id="pose">Enable pose detection</button>
<button id="hidePose">Hide pose detection</button>
</div>
</div>
<div class="canvas-container">
<video class="camera" id="video" autoplay muted playsinline></video>
<canvas class="output_canvas"></canvas>
<main></main>
</div>
<div class="blend-shapes">
<ul class="blend-shapes-list" id="video-blend-shapes"></ul>
</div>
</body>
<!-- <script defer language="javascript" type="text/javascript" src="sketch.js"></script> -->
<script src="detection.js"></script>
</html>

View file

@ -0,0 +1,75 @@
{
"shapes": [
{
"fill_H": 0,
"fill_S": 0,
"fill_B": 15,
"fill_O": 100,
"stroke_H": 263.0718090188531,
"stroke_S": 50,
"stroke_B": 0,
"stroke_O": 100,
"indices": [
152,
148,
176,
149,
150,
136,
172,
58,
132,
93,
234,
127,
162,
21,
54,
103,
67,
109,
10,
338,
297,
332,
284,
251,
389,
356,
454,
323,
361,
288,
397,
365,
379,
378,
400,
377,
152
]
},
{
"fill_H": 116.38012093685805,
"fill_S": 0,
"fill_B": 100,
"fill_O": 100,
"stroke_H": 234.7247860532549,
"stroke_S": 50,
"stroke_B": 100,
"stroke_O": 0,
"indices": [
151,
50,
280,
151,
9,
330,
101,
9,
151,
361
]
}
]
}

Some files were not shown because too many files have changed in this diff Show more