feat: Chat translator (#2931)

This commit is contained in:
HCR-750F 2024-05-11 03:41:59 +08:00 committed by GitHub
parent 9ad4885a8f
commit 0fa7c580c1
10 changed files with 285 additions and 9 deletions

View File

@ -0,0 +1,125 @@
# Setup LibreTranslate Translation on Local Computer using Docker
Yimmenu's chat translation feature relies on LibreTranslate. This guide will help you setup LibreTranslate translation service on your computer.
## Table of Contents
- [Quick Start](#quick-start)
- [1. Install Docker](#1-install-docker)
- [2. Run Docker Desktop](#2-run-docker-desktop)
- [3. Download LibreTranslate Setup Script](#3-download-libretranslate-setup-script)
- [4. Run Setup Script](#4-run-setup-script)
- [Advanced](#advanced)
- [How to Add Startup Arguments](#how-to-add-startup-arguments)
- [Common Arguments](#common-arguments)
- [1. Specify Languages to Download](#1-specify-languages-to-download)
- [2. Specify IP Address Binding](#2-specify-ip-address-binding)
- [3. Specify Listening Port](#3-specify-listening-port)
- [Removal](#removal)
- [Uninstall Docker](#uninstall-docker)
- [Remove LibreTranslate from Docker](#remove-libretranslate-from-docker)
- [Using Docker Desktop](#using-docker-desktop)
- [Using Docker Command](#using-docker-command)
## Quick Start
### 1. Install Docker
Windows: [Install Docker on Windows](https://docs.docker.com/desktop/install/windows-install/) \
Linux: [Install Docker on Linux](https://docs.docker.com/desktop/install/linux-install/) \
Mac: [Install Docker on Mac](https://docs.docker.com/desktop/install/mac-install/)
For Windows users, after running the Docker Desktop installer, simply click OK, and the installer will automatically complete all steps. If you haven't enabled WSL in control panel, restart Windows as prompted after installation to use Docker.
![docker_setup](https://github.com/sch-lda/YimMenu/assets/54973190/96b42f4e-dedc-4ba8-96af-496490325f0a)
![docker_setup_restart](https://github.com/sch-lda/YimMenu/assets/54973190/728842f0-b364-4ad6-ab24-302967fbc4db)
### 2. Run Docker Desktop
You must run Docker Desktop and complete the initialization configuration before proceeding.
Open Docker Desktop from the desktop shortcut or start menu \
Click `Accept` to agree to Docker Desktop's Terms of Service \
Click `Continue without signing in` and then `skip survey`
You can proceed to the next step when you see the green "Engine Running" indicator at the bottom left of the Docker Desktop main window.
![docker_running](https://github.com/sch-lda/YimMenu/assets/54973190/eb7f7e7e-2e05-431d-9bfa-5f7048cff588)
### 3. Download LibreTranslate Setup Script
Windows: [Download run.bat](https://raw.githubusercontent.com/LibreTranslate/LibreTranslate/main/run.bat) \
Linux/Mac: [Download run.sh](https://raw.githubusercontent.com/LibreTranslate/LibreTranslate/main/run.sh)
### 4. Run Setup Script
Running the script will automatically download LibreTranslate images and all supported languages. This process may take a few minutes.
When the console outputs `Running on http://*:5000`, it means LibreTranslate has successfully run on port 5000. You can now turn on the chat translation switch in Yimmenu and configure the target language.
To stop LibreTranslate, press Ctrl+C in the command prompt window.
After completing the initial installation, to start LibreTranslate, you only need to open Docker Desktop and then run the LibreTranslate setup script.
## Advanced
### How to Add Startup Arguments
Open Command Prompt or Linux Shell and use the `cd` command to navigate to the directory where `run.bat` or `run.sh` is located. For example, if your `run.bat` file is in `E:\git-repo\LibreTranslate\run.bat`, you need to use the command `cd /d "E:\git-repo\LibreTranslate"`.
Run with additional arguments:
Linux/macOS: `./run.sh [args]` \
Windows: `run.bat [args]`
LibreTranslate supports various parameters, see [here](https://github.com/LibreTranslate/LibreTranslate?tab=readme-ov-file#arguments) for details.
### Common Arguments
This document only lists the most likely parameters to be used as the backend service for Yimmenu chat translation.
#### 1. Specify Languages to Download
If you don't want the translation models to take up too much space, you can add parameters to specify which models to load. See the full language support list [here](https://www.argosopentech.com/argospm/index/).
For example, if you only need Chinese-English translation, you can use the command `run.bat --load-only zh,en --update-models`.
#### 2. Specify IP Address Binding
If you don't want the LibreTranslate translation service to be exposed to networks outside your local computer, you can use `--host [ip]` to modify the IP address bound by LibreTranslate. When set to 127.0.0.1, other computers on the LAN will be unable to access LibreTranslate service on your computer.
For example, `run.bat --host 127.0.0.1`.
#### 3. Specify Listening Port
If other programs need to use port 5000, you can use the `--port [port]` parameter to customize the listening port of LibreTranslate.
For example, `run.bat --port 8080`.
**If you change the listening port of LibreTranslate, please fill in the same port in the URL setting of Yimmenu chat translation settings.**
## Removal
### Uninstall Docker
If your Docker is only used to run LibreTranslate, you can uninstall Docker directly from the control panel or Windows settings after stopping LibreTranslate, and LibreTranslate will be completely removed as well.
### Remove LibreTranslate from Docker
If you wish to uninstall LibreTranslate while keeping other containers, please follow this guide.
#### Using Docker Desktop
You can easily remove all images and volumes of LibreTranslate using the GUI of Docker Desktop. Please ensure that LibreTranslate is not running.
![docker_uninstall_volumes](https://github.com/sch-lda/YimMenu/assets/54973190/bb1201dc-1fb9-4208-bda7-2dc61ac59355)
![docker_uninstall_images](https://github.com/sch-lda/YimMenu/assets/54973190/0ca02c98-a008-49db-9cf1-f36dec88c9fc)
#### Using Docker Command
If you are using Docker Engine for Linux/Mac, you can also remove LibreTranslate using docker command:
```bash
docker image rm libretranslate/libretranslate
docker volume rm lt-db
docker volume rm lt-local
```

View File

@ -125,6 +125,7 @@ namespace big
looped::session_randomize_ceo_colors();
looped::session_auto_kick_host();
looped::session_block_jobs();
looped::session_chat_translator();
if (g_script_connection_service)
g_script_connection_service->on_tick();

View File

@ -35,6 +35,7 @@ namespace big
static void session_block_jobs();
static void session_randomize_ceo_colors();
static void session_auto_kick_host();
static void session_chat_translator();
static void system_self_globals();
static void system_update_pointers();

View File

@ -0,0 +1,40 @@
#include "backend/looped/looped.hpp"
#include "util/chat.hpp"
#include "services/api/api_service.hpp"
#include "thread_pool.hpp"
namespace big
{
inline std::atomic_bool translate_lock{false};
void looped::session_chat_translator()
{
if (!translate_queue.empty() and !translate_lock and g.session.chat_translator.enabled)
{
if (translate_queue.size() >= 3)
{
LOG(WARNING) << "[Chat Translator]Message queue is too large, cleaning it. Try enabling spam timer.";
translate_queue.pop();
return;
}
auto& first_message = translate_queue.front();
translate_lock = true;
g_thread_pool->push([first_message] {
std::string translate_result;
std::string sender = "[T]" + first_message.sender;
translate_result = g_api_service->get_translation(first_message.content, g.session.chat_translator.target_language);
translate_lock = false;
if (translate_result != "")
{
if (g.session.chat_translator.draw_result)
chat::draw_chat(translate_result, sender, false);
if (g.session.chat_translator.print_result)
LOG(INFO) << "[" << first_message.sender << "]" << first_message.content << " --> " << translate_result;
}
});
translate_queue.pop();
}
}
}

View File

@ -451,7 +451,19 @@ namespace big
bool fast_join = false;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(session, log_chat_messages, log_text_messages, decloak_players, force_session_host, force_script_host, player_magnet_enabled, player_magnet_count, is_team, join_in_sctv_slots, kick_chat_spammers, kick_host_when_forcing_host, explosion_karma, damage_karma, disable_traffic, disable_peds, force_thunder, block_ceo_money, randomize_ceo_colors, block_jobs, block_muggers, block_ceo_raids, send_to_apartment_idx, send_to_warehouse_idx, chat_commands, chat_command_default_access_level, show_cheating_message, anonymous_bounty, lock_session, fast_join, unhide_players_from_player_list, allow_friends_into_locked_session, trust_friends, use_spam_timer, spam_timer, spam_length)
struct chat_translator
{
bool enabled = false;
bool print_result = false;
bool draw_result = true;
bool bypass_same_language = true;
std::string target_language = "en";
std::string endpoint = "http://localhost:5000/translate";
NLOHMANN_DEFINE_TYPE_INTRUSIVE(chat_translator, enabled, print_result, draw_result, bypass_same_language, target_language, endpoint);
} chat_translator{};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(session, log_chat_messages, log_text_messages, decloak_players, force_session_host, force_script_host, player_magnet_enabled, player_magnet_count, is_team, join_in_sctv_slots, kick_chat_spammers, kick_host_when_forcing_host, explosion_karma, damage_karma, disable_traffic, disable_peds, force_thunder, block_ceo_money, randomize_ceo_colors, block_jobs, block_muggers, block_ceo_raids, send_to_apartment_idx, send_to_warehouse_idx, chat_commands, chat_command_default_access_level, show_cheating_message, anonymous_bounty, lock_session, fast_join, unhide_players_from_player_list, allow_friends_into_locked_session, trust_friends, use_spam_timer, spam_timer, spam_length, chat_translator)
} session{};
struct settings

View File

@ -137,6 +137,11 @@ namespace big
{
if (g.session.log_chat_messages)
chat::log_chat(message, player, SpamReason::NOT_A_SPAMMER, is_team);
if (g.session.chat_translator.enabled)
{
chat_message new_message{player->get_name(), message};
translate_queue.push(new_message);
}
if (g.session.chat_commands && message[0] == g.session.chat_command_prefix)
command::process(std::string(message + 1), std::make_shared<chat_command_context>(player));

View File

@ -18,6 +18,44 @@ namespace big
g_api_service = nullptr;
}
std::string api_service::get_translation(std::string message, std::string target_language)
{
std::string url = g.session.chat_translator.endpoint;
const auto response = g_http_client.post(url,
{{"Content-Type", "application/json"}}, std::format(R"({{"q":"{}", "source":"auto", "target": "{}"}})", message, target_language));
if (response.status_code == 200)
{
try
{
nlohmann::json obj = nlohmann::json::parse(response.text);
std::string source_language = obj["detectedLanguage"]["language"];
std::string result = obj["translatedText"];
if (source_language == g.session.chat_translator.target_language && g.session.chat_translator.bypass_same_language)
return "";
return result;
}
catch (std::exception& e)
{
LOG(WARNING) << "[Chat Translator]Error when parse JSON data: " << e.what();
return "";
}
}
else if (response.status_code == 0)
{
g.session.chat_translator.enabled = false;
g_notification_service.push_error("TRANSLATOR_TOGGLE"_T.data(), "TRANSLATOR_FAILED_TO_CONNECT"_T.data());
LOG(WARNING) << "[Chat Translator]Unable to connect to LibreTranslate server. Follow the guide in Yimmenu Wiki to setup LibreTranslate server on your computer.";
}
else
{
LOG(WARNING) << "[Chat Translator]Error when sending request. Status code: " << response.status_code << " Response: " << response.text;
}
return "";
}
bool api_service::get_rid_from_username(std::string_view username, uint64_t& result)
{
const auto response = g_http_client.post("https://scui.rockstargames.com/api/friend/accountsearch", {{"Authorization", AUTHORIZATION_TICKET}, {"X-Requested-With", "XMLHttpRequest"}}, {std::format("searchNickname={}", username)});

View File

@ -12,6 +12,9 @@ namespace big
api_service();
~api_service();
// Makes an API call to a LibreTranslate endpoint.
std::string get_translation(std::string message, std::string target_language);
// Returns true if an valid profile matching his username has been found
bool get_rid_from_username(std::string_view username, uint64_t& result);

View File

@ -182,7 +182,7 @@ namespace big::chat
log.close();
}
inline void draw_chat(const char* msg, const char* player_name, bool is_team)
inline void render_chat(const char* msg, const char* player_name, bool is_team)
{
int scaleform = GRAPHICS::REQUEST_SCALEFORM_MOVIE("MULTIPLAYER_CHAT");
@ -212,6 +212,16 @@ namespace big::chat
HUD::CLOSE_MP_TEXT_CHAT();
}
inline void draw_chat(const std::string& message, const std::string& sender, bool is_team)
{
if (rage::tlsContext::get()->m_is_script_thread_active)
render_chat(message.c_str(), sender.c_str(), is_team);
else
g_fiber_pool->queue_job([message, sender, is_team] {
render_chat(message.c_str(), sender.c_str(), is_team);
});
}
inline bool is_on_same_team(CNetGamePlayer* player)
{
auto target_id = player->m_player_id;
@ -275,11 +285,17 @@ namespace big::chat
}
if (draw)
if (rage::tlsContext::get()->m_is_script_thread_active)
draw_chat(message.c_str(), g_player_service->get_self()->get_name(), is_team);
else
g_fiber_pool->queue_job([message, target, is_team] {
draw_chat(message.c_str(), g_player_service->get_self()->get_name(), is_team);
});
draw_chat(message, g_player_service->get_self()->get_name(), is_team);
}
}
namespace big
{
struct chat_message
{
std::string sender;
std::string content;
};
inline std::queue<chat_message> translate_queue;
}

View File

@ -1,4 +1,4 @@
#include "core/data/apartment_names.hpp"
#include "core/data/apartment_names.hpp"
#include "core/data/command_access_levels.hpp"
#include "core/data/region_codes.hpp"
#include "core/data/warehouse_names.hpp"
@ -26,6 +26,12 @@ namespace big
const char* name;
};
struct target_language_type
{
const char* type;
const char* name;
};
void render_rid_joiner()
{
ImGui::BeginGroup();
@ -120,6 +126,7 @@ namespace big
bool_command whitelist_friends("trustfriends", "TRUST_FRIENDS", "TRUST_FRIENDS_DESC", g.session.trust_friends);
bool_command whitelist_session("trustsession", "TRUST_SESSION", "TRUST_SESSION_DESC", g.session.trust_session);
bool_command chat_translate("translatechat", "TRANSLATOR_TOGGLE", "TRANSLATOR_TOGGLE_DESC", g.session.chat_translator.enabled);
void render_misc()
{
@ -228,6 +235,34 @@ namespace big
}
}
components::command_checkbox<"translatechat">();
if (g.session.chat_translator.enabled)
{
ImGui::Checkbox("TRANSLATOR_HIDE_SAME_LANGUAGE"_T.data(), &g.session.chat_translator.bypass_same_language);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("TRANSLATOR_HIDE_SAME_LANGUAGE_DESC"_T.data());
components::small_text("TRANSLATOR_OUTPUT"_T.data());
ImGui::Checkbox("TRANSLATOR_SHOW_ON_CHAT"_T.data(), &g.session.chat_translator.draw_result);
ImGui::Checkbox("TRANSLATOR_PRINT_TO_CONSOLE"_T.data(), &g.session.chat_translator.print_result);
static const auto target_language = std::to_array<target_language_type>({{"sq", "Albanian"}, {"ar", "Arabic"}, {"az", "Azerbaijani"}, {"bn", "Bengali"}, {"bg", "Bulgarian"}, {"ca", "Catalan"}, {"zh", "Chinese"}, {"zt", "Chinese(traditional)"}, {"cs", "Czech"}, {"da", "Danish"}, {"nl", "Dutch"}, {"en", "English"}, {"eo", "Esperanto"}, {"et", "Estonian"}, {"fi", "Finnish"}, {"fr", "French"}, {"de", "German"}, {"el", "Greek"}, {"he", "Hebrew"}, {"hi", "Hindi"}, {"hu", "Hungarian"}, {"id", "Indonesian"}, {"ga", "Irish"}, {"it", "Italian"}, {"ja", "Japanese"}, {"ko", "Korean"}, {"lv", "Latvian"}, {"lt", "Lithuanian"}, {"ms", "Malay"}, {"nb", "Norwegian"}, {"fa", "Persian"}, {"pl", "Polish"}, {"pt", "Portuguese"}, {"ro", "Romanian"}, {"ru", "Russian"}, {"sr", "Serbian"}, {"sk", "Slovak"}, {"sl", "Slovenian"}, {"es", "Spanish"}, {"sv", "Swedish"}, {"tl", "Tagalog"}, {"th", "Thai"}, {"tr", "Turkish"}, {"uk", "Ukrainian"}, {"ur", "Urdu"}, {"vi", "Vietnamese"}});
components::input_text_with_hint("TRANSLATOR_ENDPOINT"_T.data(), "http://localhost:5000/translate", g.session.chat_translator.endpoint);
if (ImGui::BeginCombo("TRANSLATOR_TARGET_LANGUAGE"_T.data(), g.session.chat_translator.target_language.c_str()))
{
for (const auto& [type, name] : target_language)
{
components::selectable(name, false, [&type] {
g.session.chat_translator.target_language = type;
});
}
ImGui::EndCombo();
}
}
ImGui::EndListBox();
}