GameSentenceMiner 2.14.9__py3-none-any.whl → 2.14.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. GameSentenceMiner/ai/__init__.py +0 -0
  2. GameSentenceMiner/ai/ai_prompting.py +473 -0
  3. GameSentenceMiner/ocr/__init__.py +0 -0
  4. GameSentenceMiner/ocr/gsm_ocr_config.py +174 -0
  5. GameSentenceMiner/ocr/ocrconfig.py +129 -0
  6. GameSentenceMiner/ocr/owocr_area_selector.py +629 -0
  7. GameSentenceMiner/ocr/owocr_helper.py +638 -0
  8. GameSentenceMiner/ocr/ss_picker.py +140 -0
  9. GameSentenceMiner/owocr/owocr/__init__.py +1 -0
  10. GameSentenceMiner/owocr/owocr/__main__.py +9 -0
  11. GameSentenceMiner/owocr/owocr/config.py +148 -0
  12. GameSentenceMiner/owocr/owocr/lens_betterproto.py +1238 -0
  13. GameSentenceMiner/owocr/owocr/ocr.py +1690 -0
  14. GameSentenceMiner/owocr/owocr/run.py +1818 -0
  15. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +109 -0
  16. GameSentenceMiner/tools/__init__.py +0 -0
  17. GameSentenceMiner/tools/audio_offset_selector.py +215 -0
  18. GameSentenceMiner/tools/ss_selector.py +135 -0
  19. GameSentenceMiner/tools/window_transparency.py +214 -0
  20. GameSentenceMiner/util/__init__.py +0 -0
  21. GameSentenceMiner/util/communication/__init__.py +22 -0
  22. GameSentenceMiner/util/communication/send.py +7 -0
  23. GameSentenceMiner/util/communication/websocket.py +94 -0
  24. GameSentenceMiner/util/configuration.py +1199 -0
  25. GameSentenceMiner/util/db.py +408 -0
  26. GameSentenceMiner/util/downloader/Untitled_json.py +472 -0
  27. GameSentenceMiner/util/downloader/__init__.py +0 -0
  28. GameSentenceMiner/util/downloader/download_tools.py +194 -0
  29. GameSentenceMiner/util/downloader/oneocr_dl.py +250 -0
  30. GameSentenceMiner/util/electron_config.py +259 -0
  31. GameSentenceMiner/util/ffmpeg.py +571 -0
  32. GameSentenceMiner/util/get_overlay_coords.py +366 -0
  33. GameSentenceMiner/util/gsm_utils.py +323 -0
  34. GameSentenceMiner/util/model.py +206 -0
  35. GameSentenceMiner/util/notification.py +157 -0
  36. GameSentenceMiner/util/text_log.py +214 -0
  37. GameSentenceMiner/util/win10toast/__init__.py +154 -0
  38. GameSentenceMiner/util/win10toast/__main__.py +22 -0
  39. GameSentenceMiner/web/__init__.py +0 -0
  40. GameSentenceMiner/web/service.py +132 -0
  41. GameSentenceMiner/web/static/__init__.py +0 -0
  42. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  43. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  44. GameSentenceMiner/web/static/favicon.ico +0 -0
  45. GameSentenceMiner/web/static/favicon.svg +3 -0
  46. GameSentenceMiner/web/static/site.webmanifest +21 -0
  47. GameSentenceMiner/web/static/style.css +292 -0
  48. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  49. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  50. GameSentenceMiner/web/templates/__init__.py +0 -0
  51. GameSentenceMiner/web/templates/index.html +50 -0
  52. GameSentenceMiner/web/templates/text_replacements.html +238 -0
  53. GameSentenceMiner/web/templates/utility.html +483 -0
  54. GameSentenceMiner/web/texthooking_page.py +584 -0
  55. GameSentenceMiner/wip/__init___.py +0 -0
  56. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/METADATA +1 -1
  57. gamesentenceminer-2.14.10.dist-info/RECORD +79 -0
  58. gamesentenceminer-2.14.9.dist-info/RECORD +0 -24
  59. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/WHEEL +0 -0
  60. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/entry_points.txt +0 -0
  61. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/licenses/LICENSE +0 -0
  62. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,250 @@
1
+ import os
2
+ import time
3
+ import zipfile
4
+ import shutil
5
+ from os.path import expanduser
6
+
7
+ import requests
8
+ import re
9
+ import tempfile
10
+
11
+ # Placeholder functions/constants for removed proprietary ones
12
+ # In a real application, you would replace these with appropriate logic
13
+ # or standard library equivalents.
14
+
15
+ def checkdir(d):
16
+ """Checks if a directory exists and contains the expected files."""
17
+ flist = ["oneocr.dll", "oneocr.onemodel", "onnxruntime.dll"]
18
+ return os.path.isdir(d) and all((os.path.isfile(os.path.join(d, _)) for _ in flist))
19
+
20
+ def selectdir():
21
+ """Attempts to find the SnippingTool directory, prioritizing cache."""
22
+ cachedir = "cache/SnippingTool"
23
+ packageFamilyName = "Microsoft.ScreenSketch_8wekyb3d8bbwe"
24
+
25
+ if checkdir(cachedir):
26
+ return cachedir
27
+ # This part needs NativeUtils.GetPackagePathByPackageFamily, which is proprietary.
28
+ # We'll skip this part for simplification as requested.
29
+ # path = NativeUtils.GetPackagePathByPackageFamily(packageFamilyName)
30
+ # if not path:
31
+ # return None
32
+ # path = os.path.join(path, "SnippingTool")
33
+ # if not checkdir(path):
34
+ # return None
35
+ # return path
36
+ return None # Return None if not found in cache
37
+
38
+ def getproxy():
39
+ """Placeholder for proxy retrieval."""
40
+ # Replace with actual proxy retrieval logic or return None
41
+ return None
42
+
43
+ def stringfyerror(e):
44
+ """Placeholder for error stringification."""
45
+ return str(e)
46
+
47
+ def dynamiclink(path):
48
+ """Placeholder for dynamic link resolution."""
49
+ # This would likely map a resource path to a local file path.
50
+ # For simplification, we'll just use the provided path string.
51
+ return path # Assuming path is a URL here based on usage
52
+
53
+ # Simplified download logic extracted from the question class
54
+ class Downloader:
55
+ def __init__(self):
56
+ self.oneocr_dir = expanduser("~/.config/oneocr")
57
+ self.packageFamilyName = "Microsoft.ScreenSketch_8wekyb3d8bbwe"
58
+ self.flist = ["oneocr.dll", "oneocr.onemodel", "onnxruntime.dll"]
59
+
60
+ def download_and_extract(self):
61
+ """
62
+ Main function to attempt download and extraction.
63
+ Tries official source first, then a fallback URL.
64
+ """
65
+ if checkdir(self.oneocr_dir):
66
+ print("Files already exist in cache.")
67
+ return True
68
+
69
+ try:
70
+ print("Attempting to download from official source...")
71
+ # raise Exception("")
72
+ self.downloadofficial()
73
+ print("Download and extraction from official source successful.")
74
+ return True
75
+ except Exception as e:
76
+ print(f"Download from official source failed: {stringfyerror(e)}")
77
+ print("Attempting to download from fallback URL...")
78
+ try:
79
+ fallback_url = "https://gsm.beangate.us/oneocr.zip"
80
+ self.downloadx(fallback_url)
81
+ print("Download and extraction from fallback URL successful.")
82
+ return True
83
+ except Exception as e_fallback:
84
+ print(f"Download from fallback URL failed: {stringfyerror(e_fallback)}")
85
+ print("All download attempts failed.")
86
+ return False
87
+
88
+
89
+ def downloadofficial(self):
90
+ """Downloads the latest SnippingTool MSIX bundle from a store API."""
91
+ headers = {
92
+ "accept": "*/*",
93
+ # Changed accept-language to prioritize US English
94
+ "accept-language": "en-US,en;q=0.9",
95
+ "cache-control": "no-cache",
96
+ "origin": "https://store.rg-adguard.net",
97
+ "pragma": "no-cache",
98
+ "priority": "u=1, i",
99
+ "referer": "https://store.rg-adguard.net/",
100
+ "sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
101
+ "sec-ch-ua-mobile": "?0",
102
+ "sec-ch-ua-platform": '"Windows"',
103
+ "sec-fetch-dest": "empty",
104
+ "sec-fetch-mode": "cors",
105
+ "sec-fetch-site": "same-origin",
106
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
107
+ }
108
+
109
+ data = dict(type="PackageFamilyName", url=self.packageFamilyName)
110
+
111
+ response = requests.post(
112
+ "https://store.rg-adguard.net/api/GetFiles",
113
+ headers=headers,
114
+ data=data,
115
+ proxies=getproxy(),
116
+ )
117
+ response.raise_for_status() # Raise an exception for bad status codes
118
+
119
+ saves = []
120
+ for link, package in re.findall('<a href="(.*?)".*?>(.*?)</a>', response.text):
121
+ if not package.startswith("Microsoft.ScreenSketch"):
122
+ continue
123
+ if not package.endswith(".msixbundle"):
124
+ continue
125
+ version = re.search(r"\d+\.\d+\.\d+\.\d+", package)
126
+ if not version:
127
+ continue
128
+ version = tuple(int(_) for _ in version.group().split("."))
129
+ saves.append((version, link, package))
130
+
131
+ if not saves:
132
+ raise Exception("Could not find suitable download link from official source.")
133
+
134
+ saves.sort(key=lambda _: _[0])
135
+ url = saves[-1][1]
136
+ package_name = saves[-1][2]
137
+
138
+ print(f"Downloading {package_name} from {url}")
139
+ req = requests.get(url, stream=True, proxies=getproxy())
140
+ req.raise_for_status()
141
+
142
+ total_size_in_bytes = int(req.headers.get('content-length', 0))
143
+ block_size = 1024 * 32 # 32 Kibibytes
144
+ temp_msixbundle_path = os.path.join(tempfile.gettempdir(), package_name)
145
+
146
+ with open(temp_msixbundle_path, "wb") as ff:
147
+ downloaded_size = 0
148
+ for chunk in req.iter_content(chunk_size=block_size):
149
+ ff.write(chunk)
150
+ downloaded_size += len(chunk)
151
+ # Basic progress reporting (can be removed)
152
+ if total_size_in_bytes:
153
+ progress = (downloaded_size / total_size_in_bytes) * 100
154
+ print(f"Downloaded {downloaded_size}/{total_size_in_bytes} bytes ({progress:.2f}%)", end='\r')
155
+ print("\nDownload complete. Extracting...")
156
+
157
+ namemsix = None
158
+ with zipfile.ZipFile(temp_msixbundle_path) as ff:
159
+ for name in ff.namelist():
160
+ if name.startswith("SnippingTool") and name.endswith("_x64.msix"):
161
+ namemsix = name
162
+ break
163
+ if not namemsix:
164
+ raise Exception("Could not find MSIX file within MSIXBUNDLE.")
165
+ temp_msix_path = os.path.join(tempfile.gettempdir(), namemsix)
166
+ ff.extract(namemsix, tempfile.gettempdir())
167
+
168
+ print(f"Extracted {namemsix}. Extracting components...")
169
+ if os.path.exists(self.oneocr_dir):
170
+ shutil.rmtree(self.oneocr_dir)
171
+ os.makedirs(self.oneocr_dir, exist_ok=True)
172
+
173
+ with zipfile.ZipFile(temp_msix_path) as ff:
174
+ collect = []
175
+ for name in ff.namelist():
176
+ # Extract only the files within the "SnippingTool/" directory
177
+ if name.startswith("SnippingTool/") and any(name.endswith(f) for f in self.flist):
178
+ # Construct target path relative to cachedir
179
+ target_path = os.path.join(self.oneocr_dir, os.path.relpath(name, "SnippingTool/"))
180
+ # Ensure parent directories exist
181
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
182
+ # Extract the file
183
+ with ff.open(name) as source, open(target_path, "wb") as target:
184
+ shutil.copyfileobj(source, target)
185
+ collect.append(name)
186
+ if not collect:
187
+ raise Exception("Could not find required files within MSIX.")
188
+
189
+
190
+ if not checkdir(self.oneocr_dir):
191
+ raise Exception("Extraction failed: Required files not found in cache directory.")
192
+
193
+ # Clean up temporary files
194
+ os.remove(temp_msixbundle_path)
195
+ os.remove(temp_msix_path)
196
+
197
+
198
+ def downloadx(self, url: str):
199
+ """Downloads a zip file from a URL and extracts it."""
200
+ print(f"Downloading from fallback URL")
201
+ # Added accept-language to the fallback download as well for consistency
202
+ headers = {
203
+ "accept-language": "en-US,en;q=0.9",
204
+ # Add other relevant headers if necessary for the fallback URL
205
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
206
+ "accept": "*/*",
207
+ }
208
+ req = requests.get(url, verify=False, proxies=getproxy(), stream=True, headers=headers)
209
+ req.raise_for_status()
210
+
211
+ total_size_in_bytes = int(req.headers.get('content-length', 0))
212
+ block_size = 1024 * 32 # 32 Kibibytes
213
+ temp_zip_path = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
214
+
215
+ with open(temp_zip_path, "wb") as ff:
216
+ downloaded_size = 0
217
+ for chunk in req.iter_content(chunk_size=block_size):
218
+ ff.write(chunk)
219
+ downloaded_size += len(chunk)
220
+ # Basic progress reporting (can be removed)
221
+ if total_size_in_bytes:
222
+ progress = (downloaded_size / total_size_in_bytes) * 100
223
+ print(f"Downloaded {downloaded_size}/{total_size_in_bytes} bytes ({progress:.2f}%)", end='\r')
224
+ print("\nDownload complete. Extracting...")
225
+
226
+ if os.path.exists(self.oneocr_dir):
227
+ shutil.rmtree(self.oneocr_dir)
228
+ os.makedirs(self.oneocr_dir, exist_ok=True)
229
+
230
+ with zipfile.ZipFile(temp_zip_path) as zipf:
231
+ zipf.extractall(self.oneocr_dir)
232
+
233
+ if not checkdir(self.oneocr_dir):
234
+ raise Exception("Extraction failed: Required files not found in cache directory.")
235
+
236
+ # Clean up temporary files
237
+ os.remove(temp_zip_path)
238
+
239
+ # Example usage:
240
+ if __name__ == "__main__":
241
+ downloader = Downloader()
242
+ downloader.download_and_extract()
243
+ # if downloader.download_and_extract():
244
+ # print("SnippingTool files are ready.")
245
+ # print("Press Ctrl+C or X on window to exit.")
246
+ # # input()
247
+ # else:
248
+ # # print("Failed to download and extract SnippingTool files. You may need to follow instructions at https://github.com/AuroraWright/oneocr")
249
+ # print("Press Ctrl+C or X on window to exit.")
250
+ # input()
@@ -0,0 +1,259 @@
1
+ import json
2
+ import os
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Optional
5
+ from dataclasses_json import dataclass_json
6
+
7
+ from GameSentenceMiner.util.configuration import get_app_directory, logger
8
+
9
+
10
+ # @dataclass_json
11
+ # @dataclass
12
+ # class SteamGame:
13
+ # id: str = ''
14
+ # name: str = ''
15
+ # processName: str = ''
16
+ # script: str = ''
17
+
18
+ @dataclass_json
19
+ @dataclass
20
+ class YuzuConfig:
21
+ emuPath: str = "C:\\Emulation\\Emulators\\yuzu-windows-msvc\\yuzu.exe"
22
+ romsPath: str = "C:\\Emulation\\Yuzu\\Games"
23
+ launchGameOnStart: str = ""
24
+ lastGameLaunched: str = ""
25
+
26
+ @dataclass_json
27
+ @dataclass
28
+ class VNConfig:
29
+ vns: List[str] = field(default_factory=list)
30
+ textractorPath: str = ""
31
+ launchVNOnStart: str = ""
32
+ lastVNLaunched: str = ""
33
+
34
+ # @dataclass_json
35
+ # @dataclass
36
+ # class SteamConfig:
37
+ # steamPath: str = ""
38
+ # steamGames: List[SteamGame] = field(default_factory=list)
39
+ # launchSteamOnStart: int = 0
40
+ # lastGameLaunched: int = 0
41
+
42
+ @dataclass_json
43
+ @dataclass
44
+ class OCRConfig:
45
+ twoPassOCR: bool = True
46
+ optimize_second_scan: bool = True
47
+ ocr1: str = "oneOCR"
48
+ ocr2: str = "glens"
49
+ window_name: str = ""
50
+ language: str = "ja"
51
+ ocr_screenshots: bool = False
52
+ furigana_filter_sensitivity: int = 0
53
+ manualOcrHotkey: str = "Ctrl+Shift+G"
54
+ areaSelectOcrHotkey: str = "Ctrl+Shift+O"
55
+ sendToClipboard: bool = True
56
+ scanRate: float = 0.5
57
+ requiresOpenWindow: bool = False
58
+ useWindowForConfig: bool = False
59
+ lastWindowSelected: str = ""
60
+ keep_newline: bool = False
61
+ useObsAsOCRSource: bool = True
62
+
63
+ def has_changed(self, other: 'OCRConfig') -> bool:
64
+ return self.to_dict() != other.to_dict()
65
+
66
+ @dataclass_json
67
+ @dataclass
68
+ class StoreConfig:
69
+ yuzu: YuzuConfig = field(default_factory=YuzuConfig)
70
+ agentScriptsPath: str = "E:\\Japanese Stuff\\agent-v0.1.4-win32-x64\\data\\scripts"
71
+ textractorPath: str = "E:\\Japanese Stuff\\Textractor\\Textractor.exe"
72
+ startConsoleMinimized: bool = False
73
+ autoUpdateElectron: bool = True
74
+ autoUpdateGSMApp: bool = False
75
+ pythonPath: str = ""
76
+ VN: VNConfig = field(default_factory=VNConfig)
77
+ # steam: SteamConfig = field(default_factory=SteamConfig)
78
+ agentPath: str = ""
79
+ OCR: OCRConfig = field(default_factory=OCRConfig)
80
+
81
+ class Store:
82
+ def __init__(self, config_path=os.path.join(get_app_directory(), "electron", "config.json"), defaults: Optional[StoreConfig] = None):
83
+ self.data: StoreConfig = StoreConfig()
84
+ self.config_path = config_path
85
+ self.defaults = defaults if defaults is not None else StoreConfig()
86
+ self._load_config()
87
+
88
+ def _load_config(self):
89
+ if os.path.exists(self.config_path):
90
+ while True:
91
+ try:
92
+ with open(self.config_path, 'r', encoding='utf-8') as f:
93
+ data = json.load(f)
94
+ self.data = StoreConfig.from_dict(data)
95
+ break
96
+ except (json.JSONDecodeError, IOError) as e:
97
+ logger.debug(f"File being written to: {e}. Retrying...")
98
+ else:
99
+ self.data = self.defaults
100
+ self._save_config()
101
+
102
+ def _save_config(self):
103
+ with open(self.config_path, 'w', encoding='utf-8') as f:
104
+ json.dump(self.data.to_dict(), f, indent=4)
105
+
106
+ def reload_config(self):
107
+ self._load_config()
108
+
109
+ def get(self, key, default=None):
110
+ keys = key.split('.')
111
+ value = self.data
112
+ for k in keys:
113
+ if hasattr(value, '__dataclass_fields__') and k in value.__dataclass_fields__:
114
+ value = getattr(value, k)
115
+ else:
116
+ return default
117
+ return value
118
+
119
+ def set(self, key, value):
120
+ keys = key.split('.')
121
+ current = self.data
122
+ for i, k in enumerate(keys):
123
+ if i == len(keys) - 1:
124
+ setattr(current, k, value)
125
+ else:
126
+ if not hasattr(current, '__dataclass_fields__') or k not in current.__dataclass_fields__:
127
+ return # Key doesn't exist in the dataclass structure
128
+ if not hasattr(getattr(current, k), '__dataclass_fields__'):
129
+ setattr(current, k, object()) # Create a new object if it's not a dataclass instance yet
130
+ current = getattr(current, k)
131
+ self._save_config()
132
+
133
+ def delete(self, key):
134
+ keys = key.split('.')
135
+ if not keys:
136
+ return False
137
+ current = self.data
138
+ for i, k in enumerate(keys[:-1]):
139
+ if not hasattr(current, '__dataclass_fields__') or k not in current.__dataclass_fields__:
140
+ return False
141
+ current = getattr(current, k)
142
+ if hasattr(current, keys[-1]):
143
+ delattr(current, keys[-1])
144
+ self._save_config()
145
+ return True
146
+ return False
147
+
148
+ def print_store(self):
149
+ """Prints the entire contents of the store in a readable JSON format."""
150
+ print(json.dumps(self.data.to_dict(), indent=4))
151
+
152
+ # Initialize the store
153
+ electron_store = Store(config_path=os.path.join(get_app_directory(), "electron", "config.json"), defaults=StoreConfig())
154
+
155
+
156
+ # def has_section_changed(section_class: type) -> bool:
157
+ # global electron_store
158
+ # # Get the attribute name from the class (e.g. OCRConfig -> OCR)
159
+ # section_name = None
160
+ # for attr, value in StoreConfig.__dataclass_fields__.items():
161
+ # if value.type == section_class:
162
+ # section_name = attr
163
+ # break
164
+ # if not section_name:
165
+ # return False
166
+ # if not os.path.exists(electron_store.config_path):
167
+ # return False
168
+ # with open(electron_store.config_path, 'r', encoding='utf-8') as f:
169
+ # data = json.load(f)
170
+ # current = StoreConfig.from_dict(data)
171
+ # current_section = getattr(current, section_name)
172
+ # old_section = getattr(electron_store, section_name)
173
+ # if hasattr(current_section, 'to_dict') and hasattr(old_section, 'to_dict'):
174
+ # return current_section.to_dict() != old_section.to_dict()
175
+ # electron_store = Store(config_path=electron_store.config_path)
176
+ # return True
177
+
178
+ # Helper Methods
179
+ def get_electron_store() -> Store:
180
+ global electron_store
181
+ return electron_store
182
+
183
+ def get_ocr_two_pass_ocr():
184
+ return electron_store.data.OCR.twoPassOCR
185
+
186
+ def get_ocr_optimize_second_scan():
187
+ return electron_store.data.OCR.optimize_second_scan
188
+
189
+ def get_ocr_ocr1():
190
+ return electron_store.data.OCR.ocr1
191
+
192
+ def get_ocr_ocr2():
193
+ return electron_store.data.OCR.ocr2
194
+
195
+ def get_ocr_window_name():
196
+ return electron_store.data.OCR.window_name or ""
197
+
198
+ def get_ocr_language():
199
+ return electron_store.data.OCR.language or "ja"
200
+
201
+ def get_ocr_ocr_screenshots():
202
+ return electron_store.data.OCR.ocr_screenshots
203
+
204
+ def get_ocr_furigana_filter_sensitivity():
205
+ return electron_store.data.OCR.furigana_filter_sensitivity
206
+
207
+ def get_ocr_manual_ocr_hotkey():
208
+ return electron_store.data.OCR.manualOcrHotkey
209
+
210
+ def get_ocr_area_select_ocr_hotkey():
211
+ return electron_store.data.OCR.areaSelectOcrHotkey
212
+
213
+ def get_ocr_send_to_clipboard():
214
+ return electron_store.data.OCR.sendToClipboard
215
+
216
+ def get_ocr_scan_rate():
217
+ return electron_store.data.OCR.scanRate
218
+
219
+ def get_ocr_requires_open_window():
220
+ return electron_store.data.OCR.requiresOpenWindow
221
+
222
+ def get_ocr_use_window_for_config():
223
+ return electron_store.data.OCR.useWindowForConfig
224
+
225
+ def get_ocr_last_window_selected():
226
+ return electron_store.data.OCR.lastWindowSelected
227
+
228
+ def get_ocr_keep_newline():
229
+ return electron_store.data.OCR.keep_newline
230
+
231
+ def get_ocr_use_obs_as_source():
232
+ return electron_store.data.OCR.useObsAsOCRSource
233
+
234
+ def get_furigana_filter_sensitivity() -> int:
235
+ return electron_store.data.OCR.furigana_filter_sensitivity
236
+
237
+ def has_ocr_config_changed() -> bool:
238
+ global electron_store
239
+ if not os.path.exists(electron_store.config_path):
240
+ return False, {}
241
+ with open(electron_store.config_path, 'r', encoding='utf-8') as f:
242
+ data = json.load(f)
243
+ current = StoreConfig.from_dict(data)
244
+ current_section = current.OCR
245
+ old_section = electron_store.data.OCR
246
+ if not (hasattr(current_section, 'to_dict') and hasattr(old_section, 'to_dict')):
247
+ return False, {}
248
+ current_dict = current_section.to_dict()
249
+ old_dict = old_section.to_dict()
250
+ if current_dict != old_dict:
251
+ changes = {k: (old_dict[k], current_dict[k]) for k in current_dict if old_dict.get(k) != current_dict.get(k)}
252
+ # logger.info(f"OCR Config changes detected: {changes}")
253
+ return True, changes
254
+ return False, {}
255
+
256
+ def reload_electron_config():
257
+ global electron_store
258
+ electron_store.reload_config()
259
+ return electron_store