Skip to content

Commit

Permalink
multi language support
Browse files Browse the repository at this point in the history
  • Loading branch information
tmarenko committed Aug 27, 2022
1 parent dadc3cf commit f1ba0a6
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 40 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ env:
OPENCV_VERSION: 4.2.0
LEPTONICA_VERSION: 1.74.3
TESSERACT_VERSION: 3.05.02
TESSERACT_ENG_DATA: https://github.com/tesseract-ocr/tessdata/raw/3.04.00/eng.traineddata
TESSERACT_LANGUAGE: eng
REPO_NAME: ${{ github.event.repository.name }}
LEPTONICA_BUILD: ${{github.workspace}}/leptonica/build
TESSERACT_BUILD: ${{github.workspace}}/tesseract/build
Expand Down Expand Up @@ -65,11 +65,11 @@ jobs:
- name: Build ${{env.REPO_NAME}}
run: |
cd ${{github.workspace}}/${{env.REPO_NAME}}
cmake -B ${{github.workspace}}/${{env.REPO_NAME}}/build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH="${{env.LEPTONICA_BUILD}};${{env.TESSERACT_BUILD}};${{env.OPENCV_BUILD}}" -DTESSERACT_ENG_DATA=${{env.TESSERACT_ENG_DATA}}
cmake -B ${{github.workspace}}/${{env.REPO_NAME}}/build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_PREFIX_PATH="${{env.LEPTONICA_BUILD}};${{env.TESSERACT_BUILD}};${{env.OPENCV_BUILD}}" -DTESSERACT_LANGUAGE=${{env.TESSERACT_LANGUAGE}}
cmake --build ${{github.workspace}}/${{env.REPO_NAME}}/build --config ${{env.BUILD_TYPE}}
- uses: actions/upload-artifact@master
if: always()
with:
name: ${{env.REPO_NAME}}-build
name: ${{env.REPO_NAME}}-${{github.ref_name}}-build
path: ${{github.workspace}}/${{env.REPO_NAME}}/build/bin
26 changes: 19 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
cmake_minimum_required(VERSION 3.21)
project(GenshinImpact_PaimonShutUp)

ADD_DEFINITIONS(-DUNICODE)
ADD_DEFINITIONS(-D_UNICODE)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO")
set(MAIN_MANIFEST "main.manifest")
if(NOT TESSERACT_ENG_DATA)
set(TESSERACT_ENG_DATA "https://github.com/tesseract-ocr/tessdata/raw/3.04.00/eng.traineddata")
set(CONFIG_FILE "settings.cfg")
if(NOT TESSERACT_LANGUAGE)
MESSAGE(STATUS "TESSERACT_LANGUAGE is empty. Using 'eng' as default")
set(TESSERACT_LANGUAGE "eng")
endif()

find_package(OpenCV 4.2.0 COMPONENTS core imgproc REQUIRED)
Expand All @@ -18,16 +23,17 @@ include_directories(${OpenCV_INCLUDE_DIRS})
include_directories(${Leptonica_INCLUDE_DIRS})
include_directories(${Tesseract_INCLUDE_DIRS})

add_executable(${PROJECT_NAME} source/main.cpp source/tesseract.cpp source/tesseract.h appicon.rc)
add_executable(${PROJECT_NAME} source/main.cpp source/tesseract.cpp source/tesseract.h appicon.rc source/config.cpp source/config.h)

MESSAGE(STATUS "LINK LIBRARIES ${OpenCV_LIBS} ${Leptonica_LIBRARIES} ${Tesseract_LIBRARIES}")
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS} ${Leptonica_LIBRARIES} libtesseract)

if (NOT EXISTS "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessdata/eng.traineddata")
MESSAGE(STATUS "Downloading Tesseract-OCR data for ENG language from: ${TESSERACT_ENG_DATA}")
if (NOT EXISTS "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessdata/${TESSERACT_LANGUAGE}.traineddata")
set(TESSERACT_LANG_DATA "https://github.com/tesseract-ocr/tessdata/raw/3.04.00/${TESSERACT_LANGUAGE}.traineddata")
MESSAGE(STATUS "Downloading Tesseract-OCR data for '${TESSERACT_LANGUAGE}' language from: ${TESSERACT_LANG_DATA}")
file(DOWNLOAD
${TESSERACT_ENG_DATA}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessdata/eng.traineddata)
${TESSERACT_LANG_DATA}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessdata/${TESSERACT_LANGUAGE}.traineddata)
endif ()

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
Expand All @@ -41,6 +47,12 @@ add_custom_command(
${CMAKE_SOURCE_DIR}/${MAIN_MANIFEST}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${MAIN_MANIFEST})

add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/${CONFIG_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CONFIG_FILE})

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND "mt.exe" -manifest "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\\${MAIN_MANIFEST}" -outputresource:"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\\${PROJECT_NAME}.exe"
COMMAND ${CMAKE_COMMAND} -E remove -f "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\\${MAIN_MANIFEST}"
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,35 @@

Automatically mutes Paimon when she speaks

#### Watch how it works: https://youtu.be/pBpDdLcw6zk

## Installation and usage

Download archive from
[latest release](https://github.com/tmarenko/GenshinImpact_PaimonShutUp/releases)
, extract it and run `GenshinImpact_PaimonShutUp.exe` with administrator privileges.
Administrator rights needed for access to Windows Audio session.

## Watch how it works
## Setup in-game language

Edit `settings.cfg` file and set your in-game language in `language` variable:

```buildoutcfg
[config]
language=eng
```

https://youtu.be/pBpDdLcw6zk
Supported languages:
* 简体中文 (`language=chi_sim`)
* 繁體中文 (`language=chi_tra`)
* English (`language=eng`)
* 한국어 (`language=kor`)
* 日本語 (`language=jpn`)
* Español (`language=spa`)
* Français (`language=fra`)
* Русский язык (`language=rus`)
* ภาษาไทย (`language=tha`)
* Tiếng Việt (`language=vie`)
* Deutsch (`language=deu`)
* Bahasa Indonesia (`language=ind`)
* Português (`language=por`)
33 changes: 33 additions & 0 deletions settings.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[config]
# available: chi_sim, chi_tra, eng, kor, jpn, spa, fra, rus, tha, vie, deu, ind, por
language=eng

[genshin_window_name]
genshin_chi_sim=原神
genshin_chi_tra=原神
genshin_eng=Genshin Impact
genshin_kor=원신
genshin_jpn=原神
genshin_spa=Genshin Impact
genshin_fra=Genshin Impact
genshin_rus=Genshin Impact
genshin_tha=Genshin Impact
genshin_vie=Genshin Impact
genshin_deu=Genshin Impact
genshin_ind=Genshin Impact
genshin_por=Genshin Impact

[paimon_name]
paimon_chi_sim=派蒙
paimon_chi_tra=派蒙
paimon_eng=Paimon
paimon_kor=폐이몬
paimon_jpn=バイモン
paimon_spa=Paimon
paimon_fra=Paimon
paimon_rus=Паймон
paimon_tha=Paimon
paimon_vie=Paimon
paimon_deu=Paimon
paimon_ind=Paimon
paimon_por=Paimon
30 changes: 30 additions & 0 deletions source/config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include "config.h"


std::map<std::string, std::string> ParseConfig(const std::string &fileName) {
std::map<std::string, std::string> configMap = {
{"language", "eng"},
{"genshin_eng", "Genshin Impact"},
{"paimon_eng", "Paimon"}
};
std::ifstream file(fileName);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
std::istringstream is_line(line);
std::string key;
if (std::getline(is_line, key, '=')) {
std::string value;
if (std::getline(is_line, value)) {
configMap[key] = value;
}
}
}
}
return configMap;
}

std::wstring convertStringToWstring(std::string &input) {
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
return converter.from_bytes(input);
}
12 changes: 12 additions & 0 deletions source/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifndef GENSHINIMPACT_PAIMONSHUTUP_CONFIG_H
#define GENSHINIMPACT_PAIMONSHUTUP_CONFIG_H

#include <fstream>
#include <sstream>
#include <map>
#include <codecvt>

std::map<std::string, std::string> ParseConfig(const std::string &fileName);
std::wstring convertStringToWstring(std::string &input);

#endif //GENSHINIMPACT_PAIMONSHUTUP_CONFIG_H
44 changes: 24 additions & 20 deletions source/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
#include "psapi.h"
#include <csignal>
#include "tesseract.h"
#include "config.h"

#define PW_RENDERFULLCONTENT 0x00000002 // Properly capture DirectComposition genshinWindow content

typedef struct GenshinWindowInfo {
const char *windowName = "Genshin Impact";
const char *windowClass = "UnityWndClass";
HWND hwnd = NULL;
std::wstring windowName = L"Genshin Impact";
std::wstring windowClass = L"UnityWndClass";
HWND hwnd = nullptr;
int width = 0;
int height = 0;
bool active = false;
Expand All @@ -31,21 +31,21 @@ const cv::Scalar DEFAULT_DIALOGUE_NAME_POS = {0.4631496915663028, 0.787213101603
const cv::Scalar OUT_DIALOGUE_NAME_POS = {0.4645968857034299, 0.7582338162046451, 0.5333868470215386, 0.8020998784944536};
const cv::Scalar DIALOGUE_NAME_COLOR_RANGE_LOW = {0, 170, 230}; // BGR
const cv::Scalar DIALOGUE_NAME_COLOR_RANGE_HIGH = {10, 210, 255}; // BGR
const char * PAIMON_NAME = "Paimon";

cv::Mat frame;
HWND genshinWindow;
GenshinWindowInfo gwi;
std::wstring genshinExe = L"GenshinImpact.exe";


BOOL CALLBACK EnumWindowsFunc(HWND hwnd, LPARAM lParam) {
auto *gwiParam = (GenshinWindowInfo *) lParam;
TCHAR buf[1024]{};
WCHAR buf[1024]{};

GetClassName(hwnd, buf, 100);
if (!lstrcmp(buf, gwiParam->windowClass)) {
if (!lstrcmp(buf, gwiParam->windowClass.c_str())) {
GetWindowText(hwnd, buf, 100);
if (!lstrcmp(buf, gwiParam->windowName)) {
if (!lstrcmp(buf, gwiParam->windowName.c_str())) {
gwiParam->hwnd = hwnd;
RECT windowRect;
GetWindowRect(hwnd, &windowRect);
Expand All @@ -61,17 +61,17 @@ void FindGenshinWindow() {
EnumWindows(EnumWindowsFunc, (LPARAM) (&gwi));
genshinWindow = gwi.hwnd;

TCHAR buff[1024]{};
WCHAR buff[1024]{};
GetWindowText(genshinWindow, buff, 100);
if (!lstrcmp(buff, gwi.windowName)) {
if (!gwi.active) {
if (!lstrcmp(buff, gwi.windowName.c_str())) {
if (gwi.hwnd && !gwi.active) {
gwi.active = true;
std::cout << "Ready for Paimon!" << std::endl;
}
} else {
if (gwi.active) {
gwi.active = false;
gwi.hwnd = NULL;
gwi.hwnd = nullptr;
std::cout << "Game was closed" << std::endl;
}
}
Expand Down Expand Up @@ -130,7 +130,7 @@ std::string GetTextFromImageByRect(const cv::Mat &image, const cv::Rect& rect) {
}


bool IsPaimonSpeaking() {
bool IsPaimonSpeaking(const std::string &paimonName) {
GetFrame(gwi.width, gwi.height);
if (frame.empty())
return false;
Expand All @@ -144,16 +144,16 @@ bool IsPaimonSpeaking() {
(int) (OUT_DIALOGUE_NAME_POS.val[3] * frame.rows) - (int) (OUT_DIALOGUE_NAME_POS.val[1] * frame.rows));
std::string defaultDialogue = GetTextFromImageByRect(frame, cropDefault);
std::string outDialogue = GetTextFromImageByRect(frame, cropOut);
return IsStringsSimilar(defaultDialogue, PAIMON_NAME) || IsStringsSimilar(outDialogue, PAIMON_NAME);
return IsStringsSimilar(defaultDialogue, paimonName) || IsStringsSimilar(outDialogue, paimonName);
}


bool IsGenshinProcess(DWORD pid) {
TCHAR buff[1024];
WCHAR buff[1024];
HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (GetProcessImageFileName(handle, reinterpret_cast<LPSTR>(buff), sizeof(buff))) {
if (GetProcessImageFileName(handle, buff, sizeof(buff))) {
CloseHandle(handle);
return ((std::string) buff).find("GenshinImpact.exe") != std::string::npos;
return std::wstring(&buff[0]).find(genshinExe) != std::wstring::npos;
}
CloseHandle(handle);
return false;
Expand Down Expand Up @@ -217,16 +217,20 @@ HRESULT SetMuteGenshin(BOOL bMute) {


int PaimonShutUp() {
if (InitTesseract(NULL, "eng"))
std::map<std::string, std::string> configMap = ParseConfig("settings.cfg");
gwi.windowName = convertStringToWstring(configMap["genshin_" + configMap["language"]]);
DownloadTessdataFileIfNecessary(configMap["language"]);
if (InitTesseract(nullptr, configMap["language"].c_str()))
return 1;

bool paimonWasHere = false;
std::string paimonName = configMap["paimon_" + configMap["language"]];
std::cout << "Waiting for GenshinImpact.exe process." << std::endl;
bool paimonWasHere = false;
while (!stop) {
FindGenshinWindow();
if (!gwi.active)
continue;
bool isPaimonSpeaking = IsPaimonSpeaking();
bool isPaimonSpeaking = IsPaimonSpeaking(paimonName);
if (isPaimonSpeaking && !paimonWasHere) {
paimonWasHere = true;
std::cout << "Paimon, shut up!" << std::endl;
Expand Down
35 changes: 28 additions & 7 deletions source/tesseract.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#include "tesseract.h"

tesseract::TessBaseAPI *tesseractApi;
const char *asciiLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const char *tesseractDownloadUrl = "https://github.com/tesseract-ocr/tessdata/raw/3.04.00/";


int InitTesseract(const char* dataPath, const char* language) {
int InitTesseract(const char *dataPath, const char *language) {
tesseractApi = new tesseract::TessBaseAPI();
if (tesseractApi->Init(dataPath, language)) {
std::cout << "Could not initialize tesseract lib." << std::endl;
Expand All @@ -15,7 +15,7 @@ int InitTesseract(const char* dataPath, const char* language) {
tesseractApi->SetVariable("tessedit_oem_mode", OEM_DEFAULT);
tesseractApi->SetVariable("debug_file", "/dev/null");

std::cout << "Initialized Tesseract API." << std::endl;
std::cout << "Initialized Tesseract API with '" << language << "' language." << std::endl;
return 0;
}

Expand All @@ -34,9 +34,9 @@ std::string StripText(const std::string &input) {
return input;
auto start_it = input.begin();
auto end_it = input.rbegin();
while (start_it != input.end() && std::isspace(*start_it))
while (start_it != input.end() && (*start_it == ' ' || *start_it == '\n'))
++start_it;
while (end_it != input.rend() && std::isspace(*end_it))
while (end_it != input.rend() && (*end_it == ' ' || *end_it == '\n'))
++end_it;
if (start_it > end_it.base())
return std::string();
Expand All @@ -50,7 +50,6 @@ std::string GetTextFromImage(const cv::Mat &image) {
return std::string();
}
char *tesseractOutText;
tesseractApi->SetVariable("tessedit_char_whitelist", asciiLetters);
tesseractApi->SetVariable("tessedit_pageseg_mode", AUTOMATIC_PAGE_SEGMENTATION);
tesseractApi->SetImage(image.data, image.cols, image.rows, image.channels(), image.cols * image.channels());
tesseractOutText = tesseractApi->GetUTF8Text();
Expand Down Expand Up @@ -78,9 +77,31 @@ unsigned int LevenshteinDistance(const std::string &s1, const std::string &s2) {
}


bool IsStringsSimilar(std::string s1, std::string s2, const double overlap) {
bool IsStringsSimilar(std::string s1, std::string s2, double overlap) {
std::transform(s1.begin(), s1.end(), s1.begin(), ::toupper);
std::transform(s2.begin(), s2.end(), s2.begin(), ::toupper);
double nonSimilarity = s1.length() > 0 ? (double) LevenshteinDistance(s1, s2) / s1.length() : 1;
return nonSimilarity <= overlap;
}


inline bool CheckFileExists(const std::string &name) {
struct stat buffer{};
return (stat(name.c_str(), &buffer) == 0);
}


void DownloadTessdataFileIfNecessary(const std::string &language) {
std::string pathToLanguage = "tessdata/" + language + ".traineddata";
if (CheckFileExists(pathToLanguage)) {
std::cout << "Found Tesseract data for '" << language << "' language at " + pathToLanguage << std::endl;
return;
}
std::string downloadUrl = tesseractDownloadUrl + language + ".traineddata";
std::cout << "Downloading Tesseract data for '" << language << "' language from " + downloadUrl << std::endl;
HRESULT hr = URLDownloadToFileA(nullptr, downloadUrl.c_str(), pathToLanguage.c_str(), 0, nullptr);
if (hr == S_OK)
std::cout << "Download complete. Saved to " + pathToLanguage << std::endl;
else
std::cout << "Error during download: " + std::to_string(hr) << std::endl;
}
Loading

0 comments on commit f1ba0a6

Please sign in to comment.