Merge pull request #5 from Khyretos/develop

Develop
This commit is contained in:
Kees Rodriguez 2024-01-05 04:34:57 +01:00 committed by GitHub
commit bc7beab074
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 6063 additions and 5570 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'
}
};

7
.gitignore vendored
View file

@ -104,4 +104,9 @@ 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

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,8 +28,6 @@ from deep_translator import (
MyMemoryTranslator,
)
import emoji
from vosk import Model, KaldiRecognizer, SetLogLevel
# global variables
@ -40,6 +45,7 @@ q = queue.Queue()
# gobal functions
# classes
class LanguageDetection:
def __init__(self):
@ -56,19 +62,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 +97,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,7 +135,8 @@ class STT:
def stop_recognition(self):
self.is_running = False
settings.read(settingsPath)
if settings["STT"]["USE_STT"] and bool(settings["STT"]["LANGUAGE"]):
speech_recognition_service = STT()
@ -151,16 +155,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 +179,13 @@ class TTS:
return [voice.name for voice in voices]
settings.read(settingsPath)
if settings["TTS"]["USE_TTS"]:
text_to_speech_service = TTS()
# endpoints
@app.route("/stream", methods=["GET"])
def stream_recognition():
def generate():
@ -194,14 +200,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 +213,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():
try:
settings.read(settingsPath)
request_data = request.json
message = request_data.get("message", "")
detectedLanguage = request_data.get("language", "")
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,18 +271,10 @@ 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)
port = int(settings["GENERAL"]["PORT"])
@ -273,5 +283,4 @@ if __name__ == "__main__":
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": {
@ -18,14 +41,16 @@
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"electron-squirrel-startup": "^1.0.0",
"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",
"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",
@ -35,9 +60,15 @@
"@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

@ -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 0px 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 0px 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,72 @@ 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;
}

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,9 +251,9 @@ body {
background-color: var(--top-bar);
}
.language-item:hover {
/* filter: brightness(150%); */
}
/* .language-item:hover {
filter: brightness(150%);
} */
@font-face {
font-family: NotoColorEmojiLimited;

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,51 @@
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);
}
.language-select {
padding: 5px;
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

@ -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 */
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -25,6 +25,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 +44,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>
@ -132,9 +134,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 +144,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 +187,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,6 +197,35 @@
<input type="text" id="ttsVolume" class="inputBox" />
</div>
</div>
<div class="AdvancedMenuRow inputTTS">
<div class="AdvancedMenuLabel">Default Internal Voice</div>
<select class="menu-select" name="primaryVoice" id="primaryVoice"></select>
</div>
<div class="AdvancedMenuRow inputTTS">
<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>
<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>
<label class="testLabel Basiclabel"></label>
</div>
</div>
</fieldset>
<fieldset id="STTMenu" class="AdvancedMenu">
@ -182,8 +240,13 @@
<select class="menu-select" name="microphone" id="microphone"></select>
</div>
<div class="AdvancedMenuRow voiceLanguageDetection inputSTT">
<div class="AdvancedMenuLabel">Voice Language</div>
<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>
@ -195,83 +258,54 @@
<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 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">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 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">Default TTS language</div>
<select
class="menu-select"
name="defaultLanguage"
id="defaultLanguage"
tip="Language Service to use"
></select>
<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">2<sup>nd</sup> TTS language</div>
<select
class="menu-select"
name="secondaryLanguage"
id="secondaryLanguage"
tip="Language Service to use"
></select>
<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="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>
</div>
<div class="AdvancedMenuRow inputTTS">
<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>
<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>
</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>
<label class="testLabel Basiclabel"></label>
</div>
</div>
</fieldset>
<fieldset id="NotificationMenu" class="AdvancedMenu">
<legend class="legendStyle" tip="Enable/Disable notification sounds">
<img class="AdvancedMenuIcon" src="./images/sound.png" alt=" " />
@ -279,6 +313,15 @@
<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 +336,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>
@ -310,6 +349,20 @@
<label for="USE_TWITCH" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Twitch</div>
</legend>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Oauth Token</div>
<input
type="password"
type2="text"
class="fname inputTwitch"
id="TWITCH_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_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" />
@ -319,24 +372,6 @@
tip="The channel you want to connect to"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Oauth Token</div>
<input
type="password"
type2="text"
class="fname inputTwitch"
id="TWITCH_OAUTH_TOKEN"
placeholder="click the ? 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 fa-question-circle fa-2x SmallButton option-icon-container"
id="Info_USERNAME"
tip="Get OAuth Token"
></i>
</div>
<div class="AdvancedMenuRow inputTwitch">
<div class="AdvancedMenuLabel">Test credentials</div>
<button type="text" class="AdvancedMenuButton" id="TestTwitchCredentials">Test</button>
@ -346,6 +381,17 @@
tip="Test Twitch credentials"
></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="AdvancedMenuServer" class="AdvancedMenu">
@ -355,6 +401,17 @@
<label for="USE_MODULES" class="toggle-small"></label>
<div class="AdvancedMenuLabel3">Enable Modules</div>
</legend>
<div class="AdvancedMenuRow inputServer" style="height: 0; visibility: hidden">
<div class="AdvancedMenuLabel">Use 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">Use Vtuber</div>
<input type="checkbox" id="USE_VTUBER" class="checkbox" />
@ -377,9 +434,20 @@
tip="You can use it as a browsersource on http://localhost:PORT/chat"
></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>
</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" />
@ -456,7 +524,7 @@
</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" />
@ -530,6 +598,26 @@
<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" />
@ -638,7 +726,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,18 +1,20 @@
/* global settings,twitch, 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'];
const express = require('express');
let tempAuthServer = express();
const tempAuthServer = express();
const port = 1989;
const { parse: parseQueryString } = require('querystring');
tempAuthServer.use(function (req, res, next) {
if (req.url !== '/auth') {
let token = parseQueryString(req.query.auth);
const token = parseQueryString(req.query.auth);
settings.TWITCH.OAUTH_TOKEN = token['#access_token'];
fs.writeFileSync(settingsPath, ini.stringify(settings));
@ -22,10 +24,6 @@ const twitchAuthentication = () =>
next();
});
function stopServer() {
tempAuthServer.close();
}
const htmlString = `
<!DOCTYPE html>
<html>
@ -62,7 +60,7 @@ const twitchAuthentication = () =>
server.listen(port, () => {
const authURL = `https://id.twitch.tv/oauth2/authorize?client_id=${settings.TWITCH.CLIENT_ID}&redirect_uri=${encodeURIComponent(
redirectUri,
redirectUri
)}&response_type=token&scope=${scopes.join(' ')}`;
shell.openExternal(authURL);
});
@ -72,30 +70,10 @@ 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();
// twitch.getTwitchChannelId();
return res;
});
}

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,259 @@ 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 (!languageBox) {
twitch.sendMessage(
`[${message.language.detectedLanguage.name} ${message.language.detectedLanguage.ISO639} > ${message.language.selectedLanguage.name} ${message.language.selectedLanguage.ISO639}] @${message.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();
}
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) {
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.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 !== detectedLanguage.ISO639) {
getTranslatedMessage({
message: message.message,
messageId: message.messageId,
remainingDetectedLanguages,
language: {
selectedLanguage: selectedPrimaryLanguage,
detectedLanguage: detectedLanguage
},
username: message.username,
formattedMessage: message.formattedMessage,
logoUrl: message.logoUrl
});
} else {
setTranslatedMessage({
originalMessage: message.message,
translation: message.message,
messageId: message.messageId,
language: {
selectedLanguage: selectedPrimaryLanguage,
detectedLanguage: selectedPrimaryLanguage
},
formattedMessage: message.formattedMessage,
username: message.username,
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 +308,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 +330,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 +350,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 +388,7 @@ async function initiateBackend() {
createBackendServer().then(() => {
getBackendServerStatus();
getInstalledVoices();
if (settings.STT.USE_STT) {
if (settings.STT.USE_STT && !settings.STT.LANGUAGE === '') {
startSTT();
}
});
@ -164,4 +417,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);
// Empty input box after sending message
document.body.querySelector('#textInput').value = '';
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 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,20 +126,19 @@ 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
}
);
};
@ -120,20 +158,19 @@ 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
}
);
};
@ -148,3 +185,7 @@ displayPanelX('.item', '#btnChatCreator', '#btnChatCreator');
// #region Show/Hide Theme Creator
// #endregion
module.exports = {
replaceChatMessageWithCustomEmojis
};

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,41 +1,26 @@
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 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();

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 axios = require('axios');
const { ipcRenderer, shell } = require('electron'); // necessary electron libraries to send data to the app
const { webFrame, ipcRenderer, shell } = require('electron'); // necessary electron libraries to send data to the app
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,20 @@ 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 twitchEmoteListSavePath =
main.isPackaged === true ? path.join(resourcesPath, './twitch-emotes.json') : path.join(resourcesPath, './config/twitch-emotes.json');
const betterTtvEmoteListSavePath =
main.isPackaged === true
? path.join(resourcesPath, './betterttv-emotes.json')
: path.join(resourcesPath, './config/betterttv-emotes.json');
// html elements
const root = document.documentElement;
@ -35,6 +39,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'));
@ -47,18 +54,19 @@ const config = require(path.join(__dirname, './js/settings'));
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 = settings.TWITCH.USE_TWITCH ? require(path.join(__dirname, './js/twitch')) : '';
let twitch = null;
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 +74,11 @@ 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;
// initialize values
config.getGeneralSettings();
@ -78,11 +91,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 +118,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 +141,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, ini.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 +246,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 +264,84 @@ 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';
};
});
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');
}
var style = getComputedStyle(usernameHtml);
var style2 = getComputedStyle(usernameHtml);
Array.from(document.body.querySelectorAll('[tip]')).forEach(el => {
addSingleTooltip(el);
});
function showChatMessage(article) {
if (article !== undefined) {
document.querySelector('#chatBox').appendChild(article);
}
const messages = document.body.querySelectorAll('.msg-container');
const lastMessage = messages[messages.length - 1];
lastMessage.scrollIntoView({ behavior: 'smooth' });
lastMessage.scrollIntoView({ block: 'end', behavior: 'smooth' });
}
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}`;
@ -253,3 +373,64 @@ hideText('.password-toggle-btn1', '#TWITCH_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, ini.stringify(settings));
document.body.querySelector('#ZOOMLEVEL').value = (settings.GENERAL.ZOOMLEVEL * 100).toFixed(0);
}
// TODO: refactor
let twitchEmotes = null;
if (fs.existsSync(twitchEmoteListSavePath)) {
const xxx = fs.readFileSync(twitchEmoteListSavePath);
twitchEmotes = JSON.parse(xxx);
emojiPicker.customEmoji = [...twitchEmotes];
}
let betterTtvEmotes = null;
if (fs.existsSync(betterTtvEmoteListSavePath)) {
const xxx = fs.readFileSync(betterTtvEmoteListSavePath);
betterTtvEmotes = JSON.parse(xxx);
emojiPicker.customEmoji = [...betterTtvEmotes];
}
if (twitchEmotes && betterTtvEmotes) {
emojiPicker.customEmoji = [...twitchEmotes, ...betterTtvEmotes];
}
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';
}
}

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%';
@ -33,8 +35,8 @@ function startChatBubbleModule() {
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('#BrowsersourceChat');
const chatframe = document.createElement('iframe');
chatframe.class = 'frame';
chatframe.src = `http://localhost:${settings.GENERAL.PORT}/chat`;
chatframe.style.width = '100%';
@ -61,15 +63,12 @@ app.use((req, res, next) => {
localServer.listen(settings.GENERAL.PORT, () => {
startVtuberModule();
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 => {});
// Receive data from the client
socket.on('xxx', (logoUrl, username, message) => {

View file

@ -1,7 +1,10 @@
/* global settings,main sttModels, 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;
@ -44,42 +50,42 @@ 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));
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));
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));
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));
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));
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));
@ -88,35 +94,75 @@ document.body.querySelector('#microphone').addEventListener('change', () => {
});
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));
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;
settings.TTS.PRIMARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
fs.writeFileSync(settingsPath, ini.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));
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;
settings.TTS.SECONDARY_TTS_LANGUAGE = select.options[select.selectedIndex].value;
fs.writeFileSync(settingsPath, ini.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, ini.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, ini.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;
@ -124,25 +170,34 @@ document.body.querySelector('#ttsAudioDevice').addEventListener('change', () =>
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, ini.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';
createNotification('Saved Channel name, please restart the application to reset twitch service', 'warning');
twitch.getTwitchChannelId();
});
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';
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));
@ -174,7 +229,7 @@ document.body.querySelector('#notification').addEventListener('change', () => {
});
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 +251,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 +291,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;
@ -250,14 +318,19 @@ 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');
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('#GetBetterTtvEmotes').addEventListener('click', async () => {
twitch.getBetterTtvGLobalEmotes();
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');
@ -391,7 +464,7 @@ document.body.querySelector('#USE_MODULES').addEventListener('click', () => {
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'
);
});
@ -403,7 +476,7 @@ document.body.querySelector('#USE_VTUBER').addEventListener('change', () => {
createNotification(
`${toggle ? 'Enabled' : 'Disabled'} Vtuber setting!
${toggle ? '' : ', the service will stop working after restarting the application'}`,
'success',
'success'
);
server.startVtuberModule();
});
@ -451,6 +524,43 @@ document.body.querySelector('#USE_STT').addEventListener('change', () => {
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, ini.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, ini.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, ini.stringify(settings));
createNotification(`${toggle ? 'Enabled' : 'Disabled'} Language detection!`, 'success');
});
function toggleLanguageDetection() {
const toggle = settings.LANGUAGE.USE_DETECTION;
const inputs = document.getElementsByClassName('languageDetectionInput');
@ -462,6 +572,14 @@ toggleLanguageDetection();
document.body.querySelector('#USE_DETECTION').addEventListener('change', () => {
const toggle = document.getElementById('USE_DETECTION').checked;
settings.LANGUAGE.USE_DETECTION = toggle;
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, ini.stringify(settings));
const inputs = document.getElementsByClassName('languageDetectionInput');
toggleRadio(toggle, inputs);
@ -486,7 +604,7 @@ document.body.querySelector('#USE_NOTIFICATION_SOUNDS').addEventListener('change
});
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));
@ -524,7 +642,7 @@ 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));
@ -562,7 +680,7 @@ 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));
@ -572,29 +690,103 @@ 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, ini.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,35 @@ 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) {
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 +131,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'
);
}

View file

@ -1,106 +1,190 @@
/* global client, playNotificationSound, chat, replaceChatMessageWithCustomEmojis, messageId, addSingleTooltip, settingsPath, fs, ini, backend, main, path, resourcesPath, customEmojis, emojiPicker,config, settings, options, sound, showChatMessage, messageTemplates, getPostTime */
const tmi = require('tmi.js');
const axios = require('axios');
let client;
let client = null;
let logoUrl = null;
const twitchChannels = [];
function sendMessage(message) {
client.say(settings.TWITCH.CHANNEL_NAME, message).catch(console.error);
}
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
.connect()
.then((data) => {})
.then(data => {})
.catch(console.error);
client.on('message', (channel, tags, message, self) => {
if (self) {
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) {
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 });
}
function getProfileImage(userid, username, message, fileteredMessage) {
// window.article = article;
}
sound.playNotificationSound();
}
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 +206,309 @@ function parseString(inputString) {
return result;
}
client.on('message', (channel, tags, message, self) => {
if (self) {
function saveTwitchEmotesToFile(TwitchEmotes) {
const data = JSON.stringify(TwitchEmotes);
const savePath =
main.isPackaged === true ? path.join(resourcesPath, './twitch-emotes.json') : path.join(resourcesPath, './config/twitch-emotes.json');
// console.log(savePath);
fs.writeFile(savePath, data, error => {
// throwing the error
// in case of a writing problem
if (error) {
// logging the error
console.error(error);
throw error;
}
// console.log('twitch-emotes.json written correctly');
});
}
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: channel.broadcaster_name
};
customEmojis.push(emojiToBeAdded);
});
emojiPicker.customEmoji = customEmojis;
saveTwitchEmotesToFile(customEmojis);
}
function saveBetterTtvEmotesToFile(BetterTtvEmotes) {
const data = JSON.stringify(BetterTtvEmotes);
const savePath =
main.isPackaged === true
? path.join(resourcesPath, './betterttv-emotes.json')
: path.join(resourcesPath, './config/betterttv-emotes.json');
fs.writeFile(savePath, data, error => {
if (error) {
console.error(error);
throw error;
}
});
}
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);
});
emojiPicker.customEmoji = customEmojis;
saveBetterTtvEmotesToFile(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 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: 'Twitch 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'
});
}
}
function getTwitchChannelId() {
options = {
method: 'GET',
url: `https://api.twitch.tv/helix/users?login=${settings.TWITCH.CHANNEL_NAME}`,
headers: {
'Client-ID': settings.TWITCH.CLIENT_ID,
Authorization: `Bearer ${settings.TWITCH.OAUTH_TOKEN}`
}
};
axios
.request(options)
.then(responseLogoUrl => {
settings.TWITCH.CHANNEL_USER_ID = responseLogoUrl.data.data[0].id;
fs.writeFileSync(settingsPath, ini.stringify(settings));
config.createNotification('Obtained channel info succesfully', 'success');
getUserAvailableTwitchEmotes();
})
.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, ini.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,
getUserAvailableTwitchEmotes,
getTwitchChannelId,
getTwitchUserId,
checkIfTokenIsValid
};

View file

@ -1,3 +1,5 @@
/* global pythonPath, a */
const { app, BrowserWindow, ipcMain } = require('electron');
const { writeIniFile } = require('write-ini-file');
const path = require('path');
@ -22,11 +24,6 @@ if (app.isPackaged) {
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);
@ -45,8 +42,8 @@ async function createWindow() {
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
enableRemoteModule: true
}
});
window.loadFile(path.join(__dirname, 'index.html'));
@ -55,7 +52,7 @@ async function createWindow() {
window.webContents.openDevTools();
}
window.on('close', (e) => {
window.on('close', e => {
settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8')); // load newest settings in case anything changed after starting the program
const bounds = window.getBounds();
@ -72,8 +69,9 @@ 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 +84,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 +92,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,18 +107,18 @@ 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 };
});
@ -132,36 +131,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 +185,22 @@ 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,
CHANNEL_NAME: '',
CHANNEL_USER_ID: '',
USERNAME: '',
USER_ID: '',
USER_LOGO_URL: '',
OAUTH_TOKEN: '',
CLIENT_ID: '2t206sj7rvtr1rutob3p627d13jch9',
CLIENT_ID: '2t206sj7rvtr1rutob3p627d13jch9'
},
MODULES: {
USE_MODULES: false,
USE_VTUBER: false,
USE_CHATBUBBLE: false,
USE_CHATBUBBLE: false
},
AMAZON: {
USE_AMAZON: false,
@ -195,15 +208,15 @@ 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,
},
CHARACTERS_USED: 0
}
}).then(() => {
settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8'));
});

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Chat</title>

View file

@ -11,7 +11,7 @@ body {
.thomas {
position: relative;
float: center;
display: inline-block;
/* display: inline-block; */
}
.speechbubble {
@ -88,10 +88,19 @@ body {
}
.msg-container {
direction: ltr;
position: static;
display: inline-block;
width: 100%;
padding-top: 10px;
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 {
@ -129,27 +138,21 @@ body {
text-align: left;
max-width: 100%;
height: auto;
min-width: 125px;
min-width: fit-content;
hyphens: auto;
bottom: 0;
right: 0;
float: right;
/* bottom: 0; */
/* right: 0; */
/* float: right; */
overflow-wrap: break-word;
}
.message {
position: relative;
border: 2px solid #ff80e1;
box-shadow: 0 2px 10px rgba(255, 128, 225, 0.5);
/* 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;
}
.message::after {
margin-bottom: 10px;
}
.arrow {
@ -157,12 +160,12 @@ body {
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;
bottom: -10px;
}
.sender {

View file

@ -1,3 +1,5 @@
/* global io */
// Connect to the Socket.IO server
const socket = io();
@ -28,16 +30,16 @@ let x;
let currentIndex = 0;
let messageStream = '';
let tempMessageObject = '';
let fullMessageLength = 0;
const tempMessageObject = '';
const fullMessageLength = 0;
function getFullMessageLength(text) {
let fullMessageLength = 0;
text.forEach((element) => {
text.forEach(element => {
if (element.text) {
fullMessageLength += element.text.length;
}
element.html;
// element.html;
fullMessageLength += 1;
});
@ -61,8 +63,8 @@ function streamText() {
}
}
function displayTwitchMessage(logoUrl, username, messageObject) {
if (!messageObject) {
function displayTwitchMessage(message) {
if (!message.filteredMessage) {
return;
}
@ -86,7 +88,7 @@ function displayTwitchMessage(logoUrl, username, messageObject) {
article.innerHTML = placeMessage;
const msg = article.querySelector('.message');
msg.innerHTML = `<div class="sender">${username}</div>`; //\n${message}`;
msg.innerHTML = `<div class="sender">${message.username}</div>`; // \n${message}`;
msg.style.fontSize = '12pt';
@ -97,27 +99,27 @@ function displayTwitchMessage(logoUrl, username, messageObject) {
elements[0].remove();
}
article.addEventListener('animationend', (e) => {
if (e.animationName == 'fade-outx') {
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].addEventListener('animationend', e => {
if (e.animationName === 'fade-outxx') {
elements[0].remove();
}
});
}
// fullMessageLength = getFullMessageLength(messageObject);
messageStream = messageObject.filtered;
messageStream = message.filteredMessage;
textStreamContainer = document.querySelector('.message');
streamText();
}
// // Receive a message from the server
socket.on('message', (logoUrl, username, message, messageDuration) => {
displayTwitchMessage(logoUrl, username, message);
socket.on('message', message => {
displayTwitchMessage(message);
});

View file

@ -20,8 +20,8 @@ body {
padding-left: 40dip;
padding-right: 10dip;
vertical-align: top;
foreground-repeat: no-repeat;
foreground-position: 16dip 50%;
/* foreground-repeat: no-repeat;
foreground-position: 16dip 50%; */
background-repeat: no-repeat;
background-position: 10dip 50%;
background-size: 64dip 64dip;
@ -37,7 +37,7 @@ body {
.title {
font-weight: bold !important;
flow: column !important;
/* flow: column !important; */
text-align: left !important;
margin: auto !important;
width: min-content !important;
@ -86,12 +86,12 @@ img {
}
#button-bar {
flow: horizontal;
/* flow: horizontal; */
padding: 10dip;
border-spacing: 10dip;
margin: 0;
flow: horizontal;
horizontal-align: right;
/* flow: horizontal; */
/* horizontal-align: right; */
}
label {
@ -103,7 +103,7 @@ label {
text-shadow: #fff 0px 1px;
min-width: 4em;
line-height: 2em;
vertical-align: middle;
/* vertical-align: middle; */
width: min-intrinsic;
text-align: center;
}

View file

@ -248,7 +248,6 @@ button.motion {
width: 50px;
}
.closed-mouth-motion {
background-image: url('../png/controls/buttons/top/motion/closed.png');
box-sizing: border-box;
@ -311,7 +310,6 @@ button.motion {
background-color: red;
}
#mouth-transition::before {
background-color: red;
background-image: url('../png/controls/buttons/top/avatar-change/border/default.png');
@ -380,7 +378,3 @@ button.motion::after {
width: 40%;
height: width(100%);
}
menu.popup {
/* box-shadow: 3px 3px 1px rgba(0, 0, 0, 0.692); */
}

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html window-frame="transparent">
<head>
<title>TransTube</title>

View file

@ -41,9 +41,7 @@ monitorDelay();
function animate() {
let avatarCurr = $('#avatar').src;
const animation = globalThis.MOUTH_IS_OPEN
? globalThis.CHOSEN_OPEN_MOUTH_ANIMATION
: globalThis.CHOSEN_CLOSED_MOUTH_ANIMATION;
const animation = globalThis.MOUTH_IS_OPEN ? globalThis.CHOSEN_OPEN_MOUTH_ANIMATION : globalThis.CHOSEN_CLOSED_MOUTH_ANIMATION;
globalThis.ANIMATION = animation;
@ -137,10 +135,7 @@ function animateButton(button, animation = 'motionless') {
});
}
animateButton(
'#closed-mouth-motion',
globalThis.CHOSEN_CLOSED_MOUTH_ANIMATION,
);
animateButton('#closed-mouth-motion', globalThis.CHOSEN_CLOSED_MOUTH_ANIMATION);
animateButton('#open-mouth-motion', globalThis.CHOSEN_OPEN_MOUTH_ANIMATION);
function blink() {
@ -177,21 +172,13 @@ async function _cycleAnimations() {
animate(key);
Window.this.caption = key;
i++;
await new Promise((r) => setTimeout(r, 2000));
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
$(document).on(
'~mousedown',
'#closed-mouth-motion, #open-mouth-motion',
motionButtonEvent,
);
$(document).on('~mousedown', '#closed-mouth-motion, #open-mouth-motion', motionButtonEvent);
$(document).on(
'~doubleclick',
'#closed-mouth-motion, #open-mouth-motion',
motionButtonEvent,
);
$(document).on('~doubleclick', '#closed-mouth-motion, #open-mouth-motion', motionButtonEvent);
function motionButtonEvent(evt) {
const { target: button } = evt;
@ -203,26 +190,10 @@ function motionButtonEvent(evt) {
button.attributes.counter--;
}
const color = [
'white',
'#9BCCD4',
'#8087D6',
'#AB65CF',
'#E7FD5B',
'#EC9F45',
'#E24555',
][mod(button.attributes.counter, 7)];
const color = ['white', '#9BCCD4', '#8087D6', '#AB65CF', '#E7FD5B', '#EC9F45', '#E24555'][mod(button.attributes.counter, 7)];
button.style.variable('color', color);
const animation = [
'motionless',
'vibing',
'shaking',
'shakingMore',
'bouncy',
'excited',
'nervous',
][mod(button.attributes.counter, 7)];
const animation = ['motionless', 'vibing', 'shaking', 'shakingMore', 'bouncy', 'excited', 'nervous'][mod(button.attributes.counter, 7)];
animateButton(button, animation);
@ -238,7 +209,8 @@ function motionButtonEvent(evt) {
function animateMouthButton() {
setInterval(() => {
const n = Date.now() % 1200;
document.body.querySelector('.mouth-transition').style.backgroundImage = `url('../vtuber/png/controls/buttons/top/motion/${n > 600 ? 'open' : 'closed'
document.body.querySelector('.mouth-transition').style.backgroundImage = `url('../vtuber/png/controls/buttons/top/motion/${
n > 600 ? 'open' : 'closed'
}.png')`;
});
}
@ -251,21 +223,16 @@ function mod(n, m) {
globalThis.CURRENT_BUTTON = null;
$(document).on(
'click',
'.mouth-image.border-default:not(:first-of-type)',
(evt, el) => {
$(document).on('click', '.mouth-image.border-default:not(:first-of-type)', (evt, el) => {
globalThis.CURRENT_BUTTON = el;
el.popup($('menu'));
},
);
});
function loadImage() {
const filename = Window.this.selectFile({
mode: 'open',
filter:
'image files (*.bmp,*.dib,*.gif,*.png,*.apng,*.jpg,*.jpeg,*.jiff)|*.bmp;*.dib;*.gif;*.png;*.apng;*.jpg;*.jpeg;*.jiff',
caption: 'select image for closed mouth...',
filter: 'image files (*.bmp,*.dib,*.gif,*.png,*.apng,*.jpg,*.jpeg,*.jiff)|*.bmp;*.dib;*.gif;*.png;*.apng;*.jpg;*.jpeg;*.jiff',
caption: 'select image for closed mouth...'
});
return filename;
}
@ -279,9 +246,7 @@ function changeImage(evt, el) {
const filename = loadImage();
if (!filename) return;
const which = globalThis.CURRENT_BUTTON.id
.match(/closed|open|blink/g)
.join('-');
const which = globalThis.CURRENT_BUTTON.id.match(/closed|open|blink/g).join('-');
document.style.variable(which, `url('${filename}')`);
globalThis.CURRENT_BUTTON.classList.add('border-default');
@ -289,11 +254,7 @@ function changeImage(evt, el) {
});
}
$(document).on(
'click',
'.mouth-image:first-of-type, .mouth-image.border-add',
changeImage,
);
$(document).on('click', '.mouth-image:first-of-type, .mouth-image.border-add', changeImage);
$(document).on('click', '#change-image', changeImage);
@ -301,9 +262,7 @@ $(document).on('click', '#remove-image', (evt, el) => {
globalThis.CURRENT_BUTTON.classList.remove('border-default');
globalThis.CURRENT_BUTTON.classList.add('border-add');
const which = globalThis.CURRENT_BUTTON.id
.match(/closed|open|blink/g)
.join('-');
const which = globalThis.CURRENT_BUTTON.id.match(/closed|open|blink/g).join('-');
document.style.variable(which, null);
});

View file

@ -7,7 +7,7 @@ export default {
shakingMore,
bouncy,
excited,
nervous,
nervous
};
function motionless(t) {
@ -25,7 +25,7 @@ function _shake(t, amount, velocity) {
num2 = num2 * 2 - 1;
return {
x: amount * (num * Math.sqrt(1 - (num2 * num2) / 2)),
y: amount * (num2 * Math.sqrt(1 - (num * num) / 2)),
y: amount * (num2 * Math.sqrt(1 - (num * num) / 2))
};
}

View file

@ -1,4 +1,5 @@
if (!Number.prototype.limit) {
// eslint-disable-next-line no-extend-native
Number.prototype.limit = function (min, max) {
if (this < min) return min;
if (this > max) return max;
@ -7,8 +8,11 @@ if (!Number.prototype.limit) {
}
function movableView(s, screenBound = false) {
let xoff; let yoff; let minXY; let maxX; let
maxY;
let xoff;
let yoff;
let minXY;
let maxX;
let maxY;
let dragging = false;
function screenBounds() {
@ -38,10 +42,7 @@ function movableView(s, screenBound = false) {
function onMouseMove(e) {
if (dragging) {
Window.this.move(
(e.screenX - xoff).limit(minXY, maxX),
(e.screenY - yoff).limit(minXY, maxY),
);
Window.this.move((e.screenX - xoff).limit(minXY, maxX), (e.screenY - yoff).limit(minXY, maxY));
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.