From 0fa7c580c1265259f9df73279bc8a0c025e8fa66 Mon Sep 17 00:00:00 2001 From: HCR-750F <54973190+sch-lda@users.noreply.github.com> Date: Sat, 11 May 2024 03:41:59 +0800 Subject: [PATCH] feat: Chat translator (#2931) --- .../setup_LibreTranslate_with_docker.md | 125 ++++++++++++++++++ src/backend/backend.cpp | 1 + src/backend/looped/looped.hpp | 1 + .../looped/session/chat_translator.cpp | 40 ++++++ src/core/settings.hpp | 14 +- src/hooks/protections/receive_net_message.cpp | 5 + src/services/api/api_service.cpp | 38 ++++++ src/services/api/api_service.hpp | 3 + src/util/chat.hpp | 30 ++++- src/views/network/view_network.cpp | 37 +++++- 10 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 docs/chat translator/setup_LibreTranslate_with_docker.md create mode 100644 src/backend/looped/session/chat_translator.cpp diff --git a/docs/chat translator/setup_LibreTranslate_with_docker.md b/docs/chat translator/setup_LibreTranslate_with_docker.md new file mode 100644 index 00000000..fd7c4a7b --- /dev/null +++ b/docs/chat translator/setup_LibreTranslate_with_docker.md @@ -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 +``` diff --git a/src/backend/backend.cpp b/src/backend/backend.cpp index de11571a..9646abdd 100644 --- a/src/backend/backend.cpp +++ b/src/backend/backend.cpp @@ -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(); diff --git a/src/backend/looped/looped.hpp b/src/backend/looped/looped.hpp index d88fc60f..0ddfbdd5 100644 --- a/src/backend/looped/looped.hpp +++ b/src/backend/looped/looped.hpp @@ -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(); diff --git a/src/backend/looped/session/chat_translator.cpp b/src/backend/looped/session/chat_translator.cpp new file mode 100644 index 00000000..a8bb97ff --- /dev/null +++ b/src/backend/looped/session/chat_translator.cpp @@ -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(); + } + } +} diff --git a/src/core/settings.hpp b/src/core/settings.hpp index 63bd1468..17a06d67 100644 --- a/src/core/settings.hpp +++ b/src/core/settings.hpp @@ -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 diff --git a/src/hooks/protections/receive_net_message.cpp b/src/hooks/protections/receive_net_message.cpp index 0ac9edb0..a7a4cebf 100644 --- a/src/hooks/protections/receive_net_message.cpp +++ b/src/hooks/protections/receive_net_message.cpp @@ -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(player)); diff --git a/src/services/api/api_service.cpp b/src/services/api/api_service.cpp index fd489879..c1917bc1 100644 --- a/src/services/api/api_service.cpp +++ b/src/services/api/api_service.cpp @@ -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)}); diff --git a/src/services/api/api_service.hpp b/src/services/api/api_service.hpp index 09a5ced0..a3e124f8 100644 --- a/src/services/api/api_service.hpp +++ b/src/services/api/api_service.hpp @@ -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); diff --git a/src/util/chat.hpp b/src/util/chat.hpp index 7d7eeeb0..f0c26c1b 100644 --- a/src/util/chat.hpp +++ b/src/util/chat.hpp @@ -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 translate_queue; +} \ No newline at end of file diff --git a/src/views/network/view_network.cpp b/src/views/network/view_network.cpp index 834ce656..8511f0eb 100644 --- a/src/views/network/view_network.cpp +++ b/src/views/network/view_network.cpp @@ -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({{"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(); }