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.
- GameSentenceMiner/ai/__init__.py +0 -0
- GameSentenceMiner/ai/ai_prompting.py +473 -0
- GameSentenceMiner/ocr/__init__.py +0 -0
- GameSentenceMiner/ocr/gsm_ocr_config.py +174 -0
- GameSentenceMiner/ocr/ocrconfig.py +129 -0
- GameSentenceMiner/ocr/owocr_area_selector.py +629 -0
- GameSentenceMiner/ocr/owocr_helper.py +638 -0
- GameSentenceMiner/ocr/ss_picker.py +140 -0
- GameSentenceMiner/owocr/owocr/__init__.py +1 -0
- GameSentenceMiner/owocr/owocr/__main__.py +9 -0
- GameSentenceMiner/owocr/owocr/config.py +148 -0
- GameSentenceMiner/owocr/owocr/lens_betterproto.py +1238 -0
- GameSentenceMiner/owocr/owocr/ocr.py +1690 -0
- GameSentenceMiner/owocr/owocr/run.py +1818 -0
- GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +109 -0
- GameSentenceMiner/tools/__init__.py +0 -0
- GameSentenceMiner/tools/audio_offset_selector.py +215 -0
- GameSentenceMiner/tools/ss_selector.py +135 -0
- GameSentenceMiner/tools/window_transparency.py +214 -0
- GameSentenceMiner/util/__init__.py +0 -0
- GameSentenceMiner/util/communication/__init__.py +22 -0
- GameSentenceMiner/util/communication/send.py +7 -0
- GameSentenceMiner/util/communication/websocket.py +94 -0
- GameSentenceMiner/util/configuration.py +1199 -0
- GameSentenceMiner/util/db.py +408 -0
- GameSentenceMiner/util/downloader/Untitled_json.py +472 -0
- GameSentenceMiner/util/downloader/__init__.py +0 -0
- GameSentenceMiner/util/downloader/download_tools.py +194 -0
- GameSentenceMiner/util/downloader/oneocr_dl.py +250 -0
- GameSentenceMiner/util/electron_config.py +259 -0
- GameSentenceMiner/util/ffmpeg.py +571 -0
- GameSentenceMiner/util/get_overlay_coords.py +366 -0
- GameSentenceMiner/util/gsm_utils.py +323 -0
- GameSentenceMiner/util/model.py +206 -0
- GameSentenceMiner/util/notification.py +157 -0
- GameSentenceMiner/util/text_log.py +214 -0
- GameSentenceMiner/util/win10toast/__init__.py +154 -0
- GameSentenceMiner/util/win10toast/__main__.py +22 -0
- GameSentenceMiner/web/__init__.py +0 -0
- GameSentenceMiner/web/service.py +132 -0
- GameSentenceMiner/web/static/__init__.py +0 -0
- GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- GameSentenceMiner/web/static/favicon.ico +0 -0
- GameSentenceMiner/web/static/favicon.svg +3 -0
- GameSentenceMiner/web/static/site.webmanifest +21 -0
- GameSentenceMiner/web/static/style.css +292 -0
- GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- GameSentenceMiner/web/templates/__init__.py +0 -0
- GameSentenceMiner/web/templates/index.html +50 -0
- GameSentenceMiner/web/templates/text_replacements.html +238 -0
- GameSentenceMiner/web/templates/utility.html +483 -0
- GameSentenceMiner/web/texthooking_page.py +584 -0
- GameSentenceMiner/wip/__init___.py +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/METADATA +1 -1
- gamesentenceminer-2.14.10.dist-info/RECORD +79 -0
- gamesentenceminer-2.14.9.dist-info/RECORD +0 -24
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1199 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import shutil
|
6
|
+
import threading
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
from logging.handlers import RotatingFileHandler
|
9
|
+
from os.path import expanduser
|
10
|
+
from sys import platform
|
11
|
+
from typing import List, Dict
|
12
|
+
import sys
|
13
|
+
from enum import Enum
|
14
|
+
|
15
|
+
import toml
|
16
|
+
from dataclasses_json import dataclass_json
|
17
|
+
|
18
|
+
from importlib import metadata
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
OFF = 'OFF'
|
23
|
+
# VOSK = 'VOSK'
|
24
|
+
SILERO = 'SILERO'
|
25
|
+
WHISPER = 'WHISPER'
|
26
|
+
# GROQ = 'GROQ'
|
27
|
+
|
28
|
+
# VOSK_BASE = 'BASE'
|
29
|
+
# VOSK_SMALL = 'SMALL'
|
30
|
+
|
31
|
+
WHISPER_TINY = 'tiny'
|
32
|
+
WHISPER_BASE = 'base'
|
33
|
+
WHISPER_SMALL = 'small'
|
34
|
+
WHISPER_MEDIUM = 'medium'
|
35
|
+
WHSIPER_LARGE = 'large'
|
36
|
+
WHISPER_TURBO = 'turbo'
|
37
|
+
|
38
|
+
AI_GEMINI = 'Gemini'
|
39
|
+
AI_GROQ = 'Groq'
|
40
|
+
AI_OPENAI = 'OpenAI'
|
41
|
+
|
42
|
+
INFO = 'INFO'
|
43
|
+
DEBUG = 'DEBUG'
|
44
|
+
|
45
|
+
DEFAULT_CONFIG = 'Default'
|
46
|
+
|
47
|
+
current_game = ''
|
48
|
+
|
49
|
+
supported_formats = {
|
50
|
+
'opus': 'libopus',
|
51
|
+
'mp3': 'libmp3lame',
|
52
|
+
'ogg': 'libvorbis',
|
53
|
+
'aac': 'aac',
|
54
|
+
'm4a': 'aac',
|
55
|
+
}
|
56
|
+
|
57
|
+
|
58
|
+
def is_linux():
|
59
|
+
return platform == 'linux'
|
60
|
+
|
61
|
+
|
62
|
+
def is_windows():
|
63
|
+
return platform == 'win32'
|
64
|
+
|
65
|
+
|
66
|
+
class Locale(Enum):
|
67
|
+
English = 'en_us'
|
68
|
+
日本語 = 'ja_jp'
|
69
|
+
한국어 = 'ko_kr'
|
70
|
+
中文 = 'zh_cn'
|
71
|
+
Español = 'es_es'
|
72
|
+
Français = 'fr_fr'
|
73
|
+
Deutsch = 'de_de'
|
74
|
+
Italiano = 'it_it'
|
75
|
+
Русский = 'ru_ru'
|
76
|
+
|
77
|
+
@classmethod
|
78
|
+
def from_any(cls, value: str) -> 'Locale':
|
79
|
+
"""
|
80
|
+
Lookup Locale by either enum name (e.g. 'English') or value (e.g. 'en_us').
|
81
|
+
Case-insensitive.
|
82
|
+
"""
|
83
|
+
value_lower = value.lower()
|
84
|
+
for locale in cls:
|
85
|
+
if locale.name.lower() == value_lower or locale.value.lower() == value_lower:
|
86
|
+
return locale
|
87
|
+
raise KeyError(f"Locale '{value}' not found.")
|
88
|
+
|
89
|
+
def __getitem__(cls, item):
|
90
|
+
try:
|
91
|
+
return cls.from_any(item)
|
92
|
+
except KeyError:
|
93
|
+
raise
|
94
|
+
|
95
|
+
|
96
|
+
# Patch Enum's __getitem__ for this class
|
97
|
+
Locale.__getitem__ = classmethod(Locale.__getitem__)
|
98
|
+
|
99
|
+
|
100
|
+
class Language(Enum):
|
101
|
+
JAPANESE = "ja"
|
102
|
+
ENGLISH = "en"
|
103
|
+
KOREAN = "ko"
|
104
|
+
CHINESE = "zh"
|
105
|
+
SPANISH = "es"
|
106
|
+
FRENCH = "fr"
|
107
|
+
GERMAN = "de"
|
108
|
+
ITALIAN = "it"
|
109
|
+
RUSSIAN = "ru"
|
110
|
+
PORTUGUESE = "pt"
|
111
|
+
HINDI = "hi"
|
112
|
+
ARABIC = "ar"
|
113
|
+
TURKISH = "tr"
|
114
|
+
DUTCH = "nl"
|
115
|
+
SWEDISH = "sv"
|
116
|
+
FINNISH = "fi"
|
117
|
+
DANISH = "da"
|
118
|
+
NORWEGIAN = "no"
|
119
|
+
|
120
|
+
|
121
|
+
AVAILABLE_LANGUAGES = [lang.value for lang in Language]
|
122
|
+
AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
|
123
|
+
|
124
|
+
|
125
|
+
class CommonLanguages(str, Enum):
|
126
|
+
"""
|
127
|
+
An Enum of the world's most common languages, based on total speaker count.
|
128
|
+
|
129
|
+
The enum member is the common English name (e.g., ENGLISH) and its
|
130
|
+
value is the ISO 639-1 two-letter code (e.g., 'en').
|
131
|
+
|
132
|
+
Inheriting from `str` allows for direct comparison and use in functions
|
133
|
+
that expect a string, e.g., `CommonLanguages.FRENCH == 'fr'`.
|
134
|
+
|
135
|
+
This list is curated from Wikipedia's "List of languages by total number of speakers"
|
136
|
+
and contains over 200 entries to provide broad but practical coverage.
|
137
|
+
"""
|
138
|
+
ENGLISH = 'en'
|
139
|
+
AFRIKAANS = 'af'
|
140
|
+
AKAN = 'ak'
|
141
|
+
ALBANIAN = 'sq'
|
142
|
+
ALGERIAN_SPOKEN_ARABIC = 'arq'
|
143
|
+
AMHARIC = 'am'
|
144
|
+
ARMENIAN = 'hy'
|
145
|
+
ASSAMESE = 'as'
|
146
|
+
BAMBARA = 'bm'
|
147
|
+
BASQUE = 'eu'
|
148
|
+
BELARUSIAN = 'be'
|
149
|
+
BENGALI = 'bn'
|
150
|
+
BHOJPURI = 'bho'
|
151
|
+
BOSNIAN = 'bs'
|
152
|
+
BODO = 'brx'
|
153
|
+
BULGARIAN = 'bg'
|
154
|
+
BURMESE = 'my'
|
155
|
+
CAPE_VERDEAN_CREOLE = 'kea'
|
156
|
+
CATALAN = 'ca'
|
157
|
+
CEBUANO = 'ceb'
|
158
|
+
CHHATTISGARHI = 'hns'
|
159
|
+
CHITTAGONIAN = 'ctg'
|
160
|
+
CROATIAN = 'hr'
|
161
|
+
CZECH = 'cs'
|
162
|
+
DANISH = 'da'
|
163
|
+
DECCAN = 'dcc'
|
164
|
+
DOGRI = 'doi'
|
165
|
+
DZONGKHA = 'dz'
|
166
|
+
DUTCH = 'nl'
|
167
|
+
EGYPTIAN_SPOKEN_ARABIC = 'arz'
|
168
|
+
ESTONIAN = 'et'
|
169
|
+
EWE = 'ee'
|
170
|
+
FAROESE = 'fo'
|
171
|
+
FIJIAN = 'fj'
|
172
|
+
FINNISH = 'fi'
|
173
|
+
FRENCH = 'fr'
|
174
|
+
GALICIAN = 'gl'
|
175
|
+
GAN_CHINESE = 'gan'
|
176
|
+
GEORGIAN = 'ka'
|
177
|
+
GERMAN = 'de'
|
178
|
+
GREEK = 'el'
|
179
|
+
GREENLANDIC = 'kl'
|
180
|
+
GUJARATI = 'gu'
|
181
|
+
HAITIAN_CREOLE = 'ht'
|
182
|
+
HAUSA = 'ha'
|
183
|
+
HAKKA_CHINESE = 'hak'
|
184
|
+
HARYANVI = 'bgc'
|
185
|
+
HEBREW = 'he'
|
186
|
+
HINDI = 'hi'
|
187
|
+
HUNGARIAN = 'hu'
|
188
|
+
ICELANDIC = 'is'
|
189
|
+
IGBO = 'ig'
|
190
|
+
INDONESIAN = 'id'
|
191
|
+
IRANIAN_PERSIAN = 'fa'
|
192
|
+
IRISH = 'ga'
|
193
|
+
ITALIAN = 'it'
|
194
|
+
JAVANESE = 'jv'
|
195
|
+
JAMAICAN_PATOIS = 'jam'
|
196
|
+
JAPANESE = 'ja'
|
197
|
+
KANNADA = 'kn'
|
198
|
+
KASHMIRI = 'ks'
|
199
|
+
KAZAKH = 'kk'
|
200
|
+
KHMER = 'km'
|
201
|
+
KONGO = 'kg'
|
202
|
+
KONKANI = 'kok'
|
203
|
+
KOREAN = 'ko'
|
204
|
+
KURDISH = 'kmr'
|
205
|
+
LAO = 'lo'
|
206
|
+
LATVIAN = 'lv'
|
207
|
+
LINGALA = 'ln'
|
208
|
+
LITHUANIAN = 'lt'
|
209
|
+
LUBA_KASAI = 'lua'
|
210
|
+
LUXEMBOURGISH = 'lb'
|
211
|
+
MACEDONIAN = 'mk'
|
212
|
+
MADURESE = 'mad'
|
213
|
+
MAGAHI = 'mag'
|
214
|
+
MAITHILI = 'mai'
|
215
|
+
MALAGASY = 'mg'
|
216
|
+
MALAYALAM = 'ml'
|
217
|
+
MALTESE = 'mt'
|
218
|
+
MANDARIN_CHINESE = 'zh'
|
219
|
+
MANIPURI = 'mni'
|
220
|
+
MARATHI = 'mr'
|
221
|
+
MAORI = 'mi'
|
222
|
+
MAURITIAN_CREOLE = 'mfe'
|
223
|
+
MIN_NAN_CHINESE = 'nan'
|
224
|
+
MINANGKABAU = 'min'
|
225
|
+
MONGOLIAN = 'mn'
|
226
|
+
MONTENEGRIN = 'cnr'
|
227
|
+
MOROCCAN_SPOKEN_ARABIC = 'ary'
|
228
|
+
NDEBELE = 'nr'
|
229
|
+
NEPALI = 'ne'
|
230
|
+
NIGERIAN_PIDGIN = 'pcm'
|
231
|
+
NORTHERN_KURDISH = 'kmr'
|
232
|
+
NORTHERN_PASHTO = 'pbu'
|
233
|
+
NORTHERN_UZBEK = 'uz'
|
234
|
+
NORWEGIAN = 'no'
|
235
|
+
ODIA = 'or'
|
236
|
+
PAPIAMENTO = 'pap'
|
237
|
+
POLISH = 'pl'
|
238
|
+
PORTUGUESE = 'pt'
|
239
|
+
ROMANIAN = 'ro'
|
240
|
+
RWANDA = 'rw'
|
241
|
+
RUSSIAN = 'ru'
|
242
|
+
SAMOAN = 'sm'
|
243
|
+
SANTALI = 'sat'
|
244
|
+
SARAIKI = 'skr'
|
245
|
+
SCOTTISH_GAELIC = 'gd'
|
246
|
+
SEYCHELLOIS_CREOLE = 'crs'
|
247
|
+
SERBIAN = 'sr'
|
248
|
+
SHONA = 'sn'
|
249
|
+
SINDHI = 'sd'
|
250
|
+
SINHALA = 'si'
|
251
|
+
SLOVAK = 'sk'
|
252
|
+
SLOVENIAN = 'sl'
|
253
|
+
SOMALI = 'so'
|
254
|
+
SOTHO = 'st'
|
255
|
+
SOUTH_AZERBAIJANI = 'azb'
|
256
|
+
SOUTHERN_PASHTO = 'ps'
|
257
|
+
SPANISH = 'es'
|
258
|
+
STANDARD_ARABIC = 'ar'
|
259
|
+
SUDANESE_SPOKEN_ARABIC = 'apd'
|
260
|
+
SUNDANESE = 'su'
|
261
|
+
SWAHILI = 'sw'
|
262
|
+
SWATI = 'ss'
|
263
|
+
SWEDISH = 'sv'
|
264
|
+
SYLHETI = 'syl'
|
265
|
+
TAGALOG = 'tl'
|
266
|
+
TAMIL = 'ta'
|
267
|
+
TELUGU = 'te'
|
268
|
+
THAI = 'th'
|
269
|
+
TIGRINYA = 'ti'
|
270
|
+
TIBETAN = 'bo'
|
271
|
+
TONGAN = 'to'
|
272
|
+
TSONGA = 'ts'
|
273
|
+
TSWANA = 'tn'
|
274
|
+
TWI = 'twi'
|
275
|
+
UKRAINIAN = 'uk'
|
276
|
+
URDU = 'ur'
|
277
|
+
UYGHUR = 'ug'
|
278
|
+
VENDA = 've'
|
279
|
+
VIETNAMESE = 'vi'
|
280
|
+
WELSH = 'cy'
|
281
|
+
WESTERN_PUNJABI = 'pnb'
|
282
|
+
WOLOF = 'wo'
|
283
|
+
WU_CHINESE = 'wuu'
|
284
|
+
XHOSA = 'xh'
|
285
|
+
YORUBA = 'yo'
|
286
|
+
YUE_CHINESE = 'yue'
|
287
|
+
ZULU = 'zu'
|
288
|
+
|
289
|
+
# Helper methods
|
290
|
+
|
291
|
+
@classmethod
|
292
|
+
def get_all_codes(cls) -> list[str]:
|
293
|
+
"""Returns a list of all language codes (e.g., ['en', 'zh', 'hi'])."""
|
294
|
+
return [lang.value for lang in cls]
|
295
|
+
|
296
|
+
@classmethod
|
297
|
+
def get_all_names(cls) -> list[str]:
|
298
|
+
"""Returns a list of all language names (e.g., ['ENGLISH', 'MANDARIN_CHINESE'])."""
|
299
|
+
return [lang.name for lang in cls]
|
300
|
+
|
301
|
+
@classmethod
|
302
|
+
def get_all_names_pretty(cls) -> list[str]:
|
303
|
+
"""Returns a list of all language names formatted for display (e.g., ['English', 'Mandarin Chinese'])."""
|
304
|
+
return [lang.name.replace('_', ' ').title() for lang in cls]
|
305
|
+
|
306
|
+
@classmethod
|
307
|
+
def get_choices(cls) -> list[tuple[str, str]]:
|
308
|
+
"""
|
309
|
+
Returns a list of (value, label) tuples for use in web framework
|
310
|
+
choice fields (e.g., Django, Flask).
|
311
|
+
|
312
|
+
Example: [('en', 'English'), ('zh', 'Mandarin Chinese')]
|
313
|
+
"""
|
314
|
+
return [(lang.value, lang.name.replace('_', ' ').title()) for lang in cls]
|
315
|
+
|
316
|
+
# Method to lookup language by it's name
|
317
|
+
@classmethod
|
318
|
+
def from_name(cls, name: str) -> 'CommonLanguages':
|
319
|
+
"""
|
320
|
+
Looks up a language by its name (e.g., 'ENGLISH') and returns the corresponding enum member.
|
321
|
+
Raises ValueError if not found.
|
322
|
+
"""
|
323
|
+
try:
|
324
|
+
return cls[name.upper()]
|
325
|
+
except KeyError:
|
326
|
+
raise ValueError(f"Language '{name}' not found in CommonLanguages")
|
327
|
+
|
328
|
+
# Method to lookup language by its code
|
329
|
+
@classmethod
|
330
|
+
def from_code(cls, code: str) -> 'CommonLanguages':
|
331
|
+
"""
|
332
|
+
Looks up a language by its code (e.g., 'en') and returns the corresponding enum member.
|
333
|
+
Raises ValueError if not found.
|
334
|
+
"""
|
335
|
+
for lang in cls:
|
336
|
+
if lang.value == code:
|
337
|
+
return lang
|
338
|
+
raise ValueError(
|
339
|
+
f"Language code '{code}' not found in CommonLanguages")
|
340
|
+
|
341
|
+
@classmethod
|
342
|
+
def name_from_code(cls, code: str) -> str:
|
343
|
+
"""
|
344
|
+
Returns the name of the language given its code (e.g., 'en' -> 'ENGLISH').
|
345
|
+
Raises ValueError if not found.
|
346
|
+
"""
|
347
|
+
return cls.from_code(code).name
|
348
|
+
|
349
|
+
|
350
|
+
PACKAGE_NAME = "GameSentenceMiner"
|
351
|
+
|
352
|
+
|
353
|
+
def get_current_version():
|
354
|
+
try:
|
355
|
+
version = metadata.version(PACKAGE_NAME)
|
356
|
+
return version
|
357
|
+
except metadata.PackageNotFoundError:
|
358
|
+
return None
|
359
|
+
|
360
|
+
|
361
|
+
def get_latest_version():
|
362
|
+
try:
|
363
|
+
import requests
|
364
|
+
response = requests.get(f"https://pypi.org/pypi/{PACKAGE_NAME}/json")
|
365
|
+
latest_version = response.json()["info"]["version"]
|
366
|
+
return latest_version
|
367
|
+
except Exception as e:
|
368
|
+
logger.error(f"Error fetching latest version: {e}")
|
369
|
+
return None
|
370
|
+
|
371
|
+
|
372
|
+
def check_for_updates(force=False):
|
373
|
+
try:
|
374
|
+
installed_version = get_current_version()
|
375
|
+
latest_version = get_latest_version()
|
376
|
+
|
377
|
+
if installed_version != latest_version or force:
|
378
|
+
logger.info(
|
379
|
+
f"Update available: {installed_version} -> {latest_version}")
|
380
|
+
return True, latest_version
|
381
|
+
else:
|
382
|
+
logger.info("You are already using the latest version.")
|
383
|
+
return False, latest_version
|
384
|
+
except Exception as e:
|
385
|
+
logger.error(f"Error checking for updates: {e}")
|
386
|
+
|
387
|
+
|
388
|
+
@dataclass_json
|
389
|
+
@dataclass
|
390
|
+
class General:
|
391
|
+
use_websocket: bool = True
|
392
|
+
use_clipboard: bool = True
|
393
|
+
use_both_clipboard_and_websocket: bool = False
|
394
|
+
merge_matching_sequential_text: bool = False
|
395
|
+
websocket_uri: str = 'localhost:6677,localhost:9001,localhost:2333'
|
396
|
+
open_config_on_startup: bool = False
|
397
|
+
open_multimine_on_startup: bool = True
|
398
|
+
texthook_replacement_regex: str = ""
|
399
|
+
texthooker_port: int = 55000
|
400
|
+
native_language: str = CommonLanguages.ENGLISH.value
|
401
|
+
|
402
|
+
def get_native_language_name(self) -> str:
|
403
|
+
try:
|
404
|
+
return CommonLanguages.name_from_code(self.native_language)
|
405
|
+
except ValueError:
|
406
|
+
return "Unknown"
|
407
|
+
|
408
|
+
|
409
|
+
@dataclass_json
|
410
|
+
@dataclass
|
411
|
+
class Paths:
|
412
|
+
folder_to_watch: str = expanduser("~/Videos/GSM")
|
413
|
+
output_folder: str = expanduser("~/Videos/GSM/Output")
|
414
|
+
copy_temp_files_to_output_folder: bool = False
|
415
|
+
open_output_folder_on_card_creation: bool = False
|
416
|
+
copy_trimmed_replay_to_output_folder: bool = False
|
417
|
+
remove_video: bool = True
|
418
|
+
remove_audio: bool = False
|
419
|
+
remove_screenshot: bool = False
|
420
|
+
|
421
|
+
def __post_init__(self):
|
422
|
+
if self.folder_to_watch:
|
423
|
+
self.folder_to_watch = os.path.normpath(self.folder_to_watch)
|
424
|
+
if self.output_folder:
|
425
|
+
self.output_folder = os.path.normpath(self.output_folder)
|
426
|
+
|
427
|
+
|
428
|
+
@dataclass_json
|
429
|
+
@dataclass
|
430
|
+
class Anki:
|
431
|
+
update_anki: bool = True
|
432
|
+
url: str = 'http://127.0.0.1:8765'
|
433
|
+
sentence_field: str = "Sentence"
|
434
|
+
sentence_audio_field: str = "SentenceAudio"
|
435
|
+
picture_field: str = "Picture"
|
436
|
+
word_field: str = 'Expression'
|
437
|
+
previous_sentence_field: str = ''
|
438
|
+
previous_image_field: str = ''
|
439
|
+
# Initialize to None and set it in __post_init__
|
440
|
+
custom_tags: List[str] = None
|
441
|
+
tags_to_check: List[str] = None
|
442
|
+
add_game_tag: bool = True
|
443
|
+
polling_rate: int = 200
|
444
|
+
overwrite_audio: bool = False
|
445
|
+
overwrite_picture: bool = True
|
446
|
+
multi_overwrites_sentence: bool = True
|
447
|
+
parent_tag: str = "Game"
|
448
|
+
|
449
|
+
def __post_init__(self):
|
450
|
+
if self.custom_tags is None:
|
451
|
+
self.custom_tags = ['GSM']
|
452
|
+
if self.tags_to_check is None:
|
453
|
+
self.tags_to_check = []
|
454
|
+
|
455
|
+
|
456
|
+
@dataclass_json
|
457
|
+
@dataclass
|
458
|
+
class Features:
|
459
|
+
full_auto: bool = True
|
460
|
+
notify_on_update: bool = True
|
461
|
+
open_anki_edit: bool = False
|
462
|
+
open_anki_in_browser: bool = True
|
463
|
+
browser_query: str = ''
|
464
|
+
backfill_audio: bool = False
|
465
|
+
|
466
|
+
|
467
|
+
@dataclass_json
|
468
|
+
@dataclass
|
469
|
+
class Screenshot:
|
470
|
+
enabled: bool = True
|
471
|
+
width: str = 0
|
472
|
+
height: str = 0
|
473
|
+
quality: str = 85
|
474
|
+
extension: str = "webp"
|
475
|
+
custom_ffmpeg_settings: str = ''
|
476
|
+
custom_ffmpeg_option_selected: str = ''
|
477
|
+
screenshot_hotkey_updates_anki: bool = False
|
478
|
+
seconds_after_line: float = 1.0
|
479
|
+
use_beginning_of_line_as_screenshot: bool = True
|
480
|
+
use_new_screenshot_logic: bool = False
|
481
|
+
screenshot_timing_setting: str = 'beginning' # 'middle', 'end'
|
482
|
+
use_screenshot_selector: bool = False
|
483
|
+
|
484
|
+
def __post_init__(self):
|
485
|
+
if not self.screenshot_timing_setting and self.use_beginning_of_line_as_screenshot:
|
486
|
+
self.screenshot_timing_setting = 'beginning'
|
487
|
+
if not self.screenshot_timing_setting and self.use_new_screenshot_logic:
|
488
|
+
self.screenshot_timing_setting = 'middle'
|
489
|
+
if not self.screenshot_timing_setting and not self.use_beginning_of_line_as_screenshot and not self.use_new_screenshot_logic:
|
490
|
+
self.screenshot_timing_setting = 'end'
|
491
|
+
if self.width and self.height == 0:
|
492
|
+
self.height = -1
|
493
|
+
if self.width == 0 and self.height:
|
494
|
+
self.width = -1
|
495
|
+
|
496
|
+
|
497
|
+
@dataclass_json
|
498
|
+
@dataclass
|
499
|
+
class Audio:
|
500
|
+
enabled: bool = True
|
501
|
+
extension: str = 'opus'
|
502
|
+
beginning_offset: float = -0.5
|
503
|
+
end_offset: float = 0.5
|
504
|
+
pre_vad_end_offset: float = 0.0
|
505
|
+
ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
|
506
|
+
ffmpeg_reencode_options_to_use: str = ''
|
507
|
+
external_tool: str = ""
|
508
|
+
anki_media_collection: str = ""
|
509
|
+
external_tool_enabled: bool = True
|
510
|
+
custom_encode_settings: str = ''
|
511
|
+
|
512
|
+
def __post_init__(self):
|
513
|
+
self.ffmpeg_reencode_options_to_use = self.ffmpeg_reencode_options.replace(
|
514
|
+
"{format}", self.extension).replace("{encoder}", supported_formats.get(self.extension, ''))
|
515
|
+
if not self.anki_media_collection:
|
516
|
+
self.anki_media_collection = get_default_anki_media_collection_path()
|
517
|
+
if self.anki_media_collection:
|
518
|
+
self.anki_media_collection = os.path.normpath(
|
519
|
+
self.anki_media_collection)
|
520
|
+
if self.external_tool:
|
521
|
+
self.external_tool = os.path.normpath(self.external_tool)
|
522
|
+
|
523
|
+
|
524
|
+
@dataclass_json
|
525
|
+
@dataclass
|
526
|
+
class OBS:
|
527
|
+
open_obs: bool = True
|
528
|
+
close_obs: bool = True
|
529
|
+
host: str = "127.0.0.1"
|
530
|
+
port: int = 7274
|
531
|
+
password: str = "your_password"
|
532
|
+
get_game_from_scene: bool = True
|
533
|
+
minimum_replay_size: int = 0
|
534
|
+
turn_off_output_check: bool = False
|
535
|
+
|
536
|
+
|
537
|
+
@dataclass_json
|
538
|
+
@dataclass
|
539
|
+
class Hotkeys:
|
540
|
+
reset_line: str = 'f5'
|
541
|
+
take_screenshot: str = 'f6'
|
542
|
+
open_utility: str = 'ctrl+m'
|
543
|
+
play_latest_audio: str = 'f7'
|
544
|
+
|
545
|
+
|
546
|
+
@dataclass_json
|
547
|
+
@dataclass
|
548
|
+
class VAD:
|
549
|
+
whisper_model: str = WHISPER_BASE
|
550
|
+
do_vad_postprocessing: bool = True
|
551
|
+
language: str = 'ja'
|
552
|
+
# vosk_url: str = VOSK_BASE
|
553
|
+
selected_vad_model: str = WHISPER
|
554
|
+
backup_vad_model: str = SILERO
|
555
|
+
trim_beginning: bool = False
|
556
|
+
beginning_offset: float = -0.25
|
557
|
+
add_audio_on_no_results: bool = False
|
558
|
+
cut_and_splice_segments: bool = False
|
559
|
+
splice_padding: float = 0.1
|
560
|
+
|
561
|
+
def is_silero(self):
|
562
|
+
return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
|
563
|
+
|
564
|
+
def is_whisper(self):
|
565
|
+
return self.selected_vad_model == WHISPER or self.backup_vad_model == WHISPER
|
566
|
+
|
567
|
+
# def is_vosk(self):
|
568
|
+
# return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
|
569
|
+
|
570
|
+
# def is_groq(self):
|
571
|
+
# return self.selected_vad_model == GROQ or self.backup_vad_model == GROQ
|
572
|
+
|
573
|
+
|
574
|
+
@dataclass_json
|
575
|
+
@dataclass
|
576
|
+
class Advanced:
|
577
|
+
plaintext_websocket_port: int = -1
|
578
|
+
audio_player_path: str = ''
|
579
|
+
video_player_path: str = ''
|
580
|
+
show_screenshot_buttons: bool = False
|
581
|
+
multi_line_line_break: str = '<br>'
|
582
|
+
multi_line_sentence_storage_field: str = ''
|
583
|
+
ocr_websocket_port: int = 9002
|
584
|
+
texthooker_communication_websocket_port: int = 55001
|
585
|
+
|
586
|
+
def __post_init__(self):
|
587
|
+
if self.plaintext_websocket_port == -1:
|
588
|
+
self.plaintext_websocket_port = self.texthooker_communication_websocket_port + 1
|
589
|
+
|
590
|
+
|
591
|
+
@dataclass_json
|
592
|
+
@dataclass
|
593
|
+
class Ai:
|
594
|
+
enabled: bool = False
|
595
|
+
anki_field: str = ''
|
596
|
+
provider: str = AI_GEMINI
|
597
|
+
gemini_model: str = 'gemini-2.5-flash-lite'
|
598
|
+
groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
599
|
+
gemini_api_key: str = ''
|
600
|
+
api_key: str = '' # Legacy support, will be moved to gemini_api_key if provider is gemini
|
601
|
+
groq_api_key: str = ''
|
602
|
+
open_ai_url: str = ''
|
603
|
+
open_ai_model: str = ''
|
604
|
+
open_ai_api_key: str = ''
|
605
|
+
use_canned_translation_prompt: bool = True
|
606
|
+
use_canned_context_prompt: bool = False
|
607
|
+
custom_prompt: str = ''
|
608
|
+
dialogue_context_length: int = 10
|
609
|
+
|
610
|
+
def __post_init__(self):
|
611
|
+
if not self.gemini_api_key:
|
612
|
+
self.gemini_api_key = self.api_key
|
613
|
+
if self.provider == 'gemini':
|
614
|
+
self.provider = AI_GEMINI
|
615
|
+
if self.provider == 'groq':
|
616
|
+
self.provider = AI_GROQ
|
617
|
+
if self.gemini_model in ['RECOMMENDED', 'OTHER']:
|
618
|
+
self.gemini_model = 'gemini-2.5-flash-lite'
|
619
|
+
if self.groq_model in ['RECOMMENDED', 'OTHER']:
|
620
|
+
self.groq_model = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
621
|
+
|
622
|
+
# Change Legacy Model Name
|
623
|
+
if self.gemini_model == 'gemini-2.5-flash-lite-preview-06-17':
|
624
|
+
self.gemini_model = 'gemini-2.5-flash-lite'
|
625
|
+
|
626
|
+
|
627
|
+
@dataclass_json
|
628
|
+
@dataclass
|
629
|
+
class Overlay:
|
630
|
+
websocket_port: int = 55499
|
631
|
+
monitor_to_capture: int = 0
|
632
|
+
|
633
|
+
def __post_init__(self):
|
634
|
+
if self.monitor_to_capture == -1:
|
635
|
+
self.monitor_to_capture = 0 # Default to the first monitor if not set
|
636
|
+
|
637
|
+
|
638
|
+
@dataclass_json
|
639
|
+
@dataclass
|
640
|
+
class WIP:
|
641
|
+
pass
|
642
|
+
|
643
|
+
|
644
|
+
@dataclass_json
|
645
|
+
@dataclass
|
646
|
+
class ProfileConfig:
|
647
|
+
name: str = 'Default'
|
648
|
+
scenes: List[str] = field(default_factory=list)
|
649
|
+
general: General = field(default_factory=General)
|
650
|
+
paths: Paths = field(default_factory=Paths)
|
651
|
+
anki: Anki = field(default_factory=Anki)
|
652
|
+
features: Features = field(default_factory=Features)
|
653
|
+
screenshot: Screenshot = field(default_factory=Screenshot)
|
654
|
+
audio: Audio = field(default_factory=Audio)
|
655
|
+
obs: OBS = field(default_factory=OBS)
|
656
|
+
hotkeys: Hotkeys = field(default_factory=Hotkeys)
|
657
|
+
vad: VAD = field(default_factory=VAD)
|
658
|
+
advanced: Advanced = field(default_factory=Advanced)
|
659
|
+
ai: Ai = field(default_factory=Ai)
|
660
|
+
overlay: Overlay = field(default_factory=Overlay)
|
661
|
+
wip: WIP = field(default_factory=WIP)
|
662
|
+
|
663
|
+
def get_field_value(self, section: str, field_name: str):
|
664
|
+
section_obj = getattr(self, section, None)
|
665
|
+
if section_obj and hasattr(section_obj, field_name):
|
666
|
+
return getattr(section_obj, field_name)
|
667
|
+
else:
|
668
|
+
raise ValueError(
|
669
|
+
f"Field '{field_name}' not found in section '{section}' of ProfileConfig.")
|
670
|
+
|
671
|
+
# This is just for legacy support
|
672
|
+
def load_from_toml(self, file_path: str):
|
673
|
+
with open(file_path, 'r') as f:
|
674
|
+
config_data = toml.load(f)
|
675
|
+
|
676
|
+
self.paths.folder_to_watch = expanduser(config_data['paths'].get(
|
677
|
+
'folder_to_watch', self.paths.folder_to_watch))
|
678
|
+
|
679
|
+
self.anki.url = config_data['anki'].get('url', self.anki.url)
|
680
|
+
self.anki.sentence_field = config_data['anki'].get(
|
681
|
+
'sentence_field', self.anki.sentence_field)
|
682
|
+
self.anki.sentence_audio_field = config_data['anki'].get(
|
683
|
+
'sentence_audio_field', self.anki.sentence_audio_field)
|
684
|
+
self.anki.word_field = config_data['anki'].get(
|
685
|
+
'word_field', self.anki.word_field)
|
686
|
+
self.anki.picture_field = config_data['anki'].get(
|
687
|
+
'picture_field', self.anki.picture_field)
|
688
|
+
self.anki.custom_tags = config_data['anki'].get(
|
689
|
+
'custom_tags', self.anki.custom_tags)
|
690
|
+
self.anki.add_game_tag = config_data['anki'].get(
|
691
|
+
'add_game_tag', self.anki.add_game_tag)
|
692
|
+
self.anki.polling_rate = config_data['anki'].get(
|
693
|
+
'polling_rate', self.anki.polling_rate)
|
694
|
+
self.anki.overwrite_audio = config_data['anki_overwrites'].get(
|
695
|
+
'overwrite_audio', self.anki.overwrite_audio)
|
696
|
+
self.anki.overwrite_picture = config_data['anki_overwrites'].get('overwrite_picture',
|
697
|
+
self.anki.overwrite_picture)
|
698
|
+
|
699
|
+
self.features.full_auto = config_data['features'].get(
|
700
|
+
'do_vosk_postprocessing', self.features.full_auto)
|
701
|
+
self.features.notify_on_update = config_data['features'].get(
|
702
|
+
'notify_on_update', self.features.notify_on_update)
|
703
|
+
self.features.open_anki_edit = config_data['features'].get(
|
704
|
+
'open_anki_edit', self.features.open_anki_edit)
|
705
|
+
self.features.backfill_audio = config_data['features'].get(
|
706
|
+
'backfill_audio', self.features.backfill_audio)
|
707
|
+
|
708
|
+
self.screenshot.width = config_data['screenshot'].get(
|
709
|
+
'width', self.screenshot.width)
|
710
|
+
self.screenshot.height = config_data['screenshot'].get(
|
711
|
+
'height', self.screenshot.height)
|
712
|
+
self.screenshot.quality = config_data['screenshot'].get(
|
713
|
+
'quality', self.screenshot.quality)
|
714
|
+
self.screenshot.extension = config_data['screenshot'].get(
|
715
|
+
'extension', self.screenshot.extension)
|
716
|
+
self.screenshot.custom_ffmpeg_settings = config_data['screenshot'].get('custom_ffmpeg_settings',
|
717
|
+
self.screenshot.custom_ffmpeg_settings)
|
718
|
+
|
719
|
+
self.audio.extension = config_data['audio'].get(
|
720
|
+
'extension', self.audio.extension)
|
721
|
+
self.audio.beginning_offset = config_data['audio'].get(
|
722
|
+
'beginning_offset', self.audio.beginning_offset)
|
723
|
+
self.audio.end_offset = config_data['audio'].get(
|
724
|
+
'end_offset', self.audio.end_offset)
|
725
|
+
self.audio.ffmpeg_reencode_options = config_data['audio'].get('ffmpeg_reencode_options',
|
726
|
+
self.audio.ffmpeg_reencode_options)
|
727
|
+
|
728
|
+
self.vad.whisper_model = config_data['vosk'].get(
|
729
|
+
'whisper_model', self.vad.whisper_model)
|
730
|
+
self.vad.vosk_url = config_data['vosk'].get('url', self.vad.vosk_url)
|
731
|
+
self.vad.do_vad_postprocessing = config_data['features'].get('do_vosk_postprocessing',
|
732
|
+
self.vad.do_vad_postprocessing)
|
733
|
+
self.vad.trim_beginning = config_data['audio'].get(
|
734
|
+
'vosk_trim_beginning', self.vad.trim_beginning)
|
735
|
+
|
736
|
+
self.obs.host = config_data['obs'].get('host', self.obs.host)
|
737
|
+
self.obs.port = config_data['obs'].get('port', self.obs.port)
|
738
|
+
self.obs.password = config_data['obs'].get(
|
739
|
+
'password', self.obs.password)
|
740
|
+
|
741
|
+
self.general.use_websocket = config_data['websocket'].get(
|
742
|
+
'enabled', self.general.use_websocket)
|
743
|
+
self.general.websocket_uri = config_data['websocket'].get(
|
744
|
+
'uri', self.general.websocket_uri)
|
745
|
+
|
746
|
+
self.hotkeys.reset_line = config_data['hotkeys'].get(
|
747
|
+
'reset_line', self.hotkeys.reset_line)
|
748
|
+
self.hotkeys.take_screenshot = config_data['hotkeys'].get(
|
749
|
+
'take_screenshot', self.hotkeys.take_screenshot)
|
750
|
+
|
751
|
+
with open(get_config_path(), 'w') as f:
|
752
|
+
f.write(self.to_json(indent=4))
|
753
|
+
print(
|
754
|
+
'config.json successfully generated from previous settings. config.toml will no longer be used.')
|
755
|
+
|
756
|
+
return self
|
757
|
+
|
758
|
+
def restart_required(self, previous):
|
759
|
+
previous: ProfileConfig
|
760
|
+
if any([previous.paths.folder_to_watch != self.paths.folder_to_watch,
|
761
|
+
previous.obs.open_obs != self.obs.open_obs,
|
762
|
+
previous.obs.host != self.obs.host,
|
763
|
+
previous.obs.port != self.obs.port
|
764
|
+
]):
|
765
|
+
logger.info("Restart Required for Some Settings that were Changed")
|
766
|
+
return True
|
767
|
+
return False
|
768
|
+
|
769
|
+
def config_changed(self, new: 'ProfileConfig') -> bool:
|
770
|
+
return self != new
|
771
|
+
|
772
|
+
|
773
|
+
@dataclass_json
|
774
|
+
@dataclass
|
775
|
+
class Config:
|
776
|
+
configs: Dict[str, ProfileConfig] = field(default_factory=dict)
|
777
|
+
current_profile: str = DEFAULT_CONFIG
|
778
|
+
switch_to_default_if_not_found: bool = True
|
779
|
+
locale: str = Locale.English.value
|
780
|
+
|
781
|
+
@classmethod
|
782
|
+
def new(cls):
|
783
|
+
instance = cls(
|
784
|
+
configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
|
785
|
+
return instance
|
786
|
+
|
787
|
+
def get_locale(self) -> Locale:
|
788
|
+
try:
|
789
|
+
return Locale.from_any(self.locale)
|
790
|
+
except KeyError:
|
791
|
+
logger.warning(
|
792
|
+
f"Locale '{self.locale}' not found. Defaulting to English.")
|
793
|
+
return Locale.English
|
794
|
+
|
795
|
+
@classmethod
|
796
|
+
def load(cls):
|
797
|
+
config_path = get_config_path()
|
798
|
+
if os.path.exists(config_path):
|
799
|
+
with open(config_path, 'r') as file:
|
800
|
+
data = json.load(file)
|
801
|
+
return cls.from_dict(data)
|
802
|
+
else:
|
803
|
+
return cls.new()
|
804
|
+
|
805
|
+
def save(self):
|
806
|
+
with open(get_config_path(), 'w') as file:
|
807
|
+
json.dump(self.to_dict(), file, indent=4)
|
808
|
+
return self
|
809
|
+
|
810
|
+
def get_config(self) -> ProfileConfig:
|
811
|
+
if self.current_profile not in self.configs:
|
812
|
+
logger.warning(
|
813
|
+
f"Profile '{self.current_profile}' not found. Switching to default profile.")
|
814
|
+
self.current_profile = DEFAULT_CONFIG
|
815
|
+
return self.configs[self.current_profile]
|
816
|
+
|
817
|
+
def set_config_for_profile(self, profile: str, config: ProfileConfig):
|
818
|
+
config.name = profile
|
819
|
+
self.configs[profile] = config
|
820
|
+
|
821
|
+
def has_config_for_current_game(self):
|
822
|
+
return current_game in self.configs
|
823
|
+
|
824
|
+
def get_all_profile_names(self):
|
825
|
+
return list(self.configs.keys())
|
826
|
+
|
827
|
+
def get_default_config(self):
|
828
|
+
return self.configs[DEFAULT_CONFIG]
|
829
|
+
|
830
|
+
def sync_changed_fields(self, previous_config: ProfileConfig):
|
831
|
+
current_config = self.get_config()
|
832
|
+
|
833
|
+
for section in current_config.to_dict():
|
834
|
+
if dataclasses.is_dataclass(getattr(current_config, section, None)):
|
835
|
+
for field_name in getattr(current_config, section, None).to_dict():
|
836
|
+
config_section = getattr(current_config, section, None)
|
837
|
+
previous_config_section = getattr(
|
838
|
+
previous_config, section, None)
|
839
|
+
current_value = getattr(config_section, field_name, None)
|
840
|
+
previous_value = getattr(
|
841
|
+
previous_config_section, field_name, None)
|
842
|
+
if str(current_value).strip() != str(previous_value).strip():
|
843
|
+
logger.info(
|
844
|
+
f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
|
845
|
+
for profile in self.configs.values():
|
846
|
+
if profile != current_config:
|
847
|
+
profile_section = getattr(
|
848
|
+
profile, section, None)
|
849
|
+
if profile_section:
|
850
|
+
setattr(profile_section,
|
851
|
+
field_name, current_value)
|
852
|
+
logger.info(
|
853
|
+
f"Updated '{field_name}' in profile '{profile.name}'")
|
854
|
+
|
855
|
+
return self
|
856
|
+
|
857
|
+
def sync_shared_fields(self):
|
858
|
+
config = self.get_config()
|
859
|
+
for profile in self.configs.values():
|
860
|
+
self.sync_shared_field(
|
861
|
+
config.hotkeys, profile.hotkeys, "reset_line")
|
862
|
+
self.sync_shared_field(
|
863
|
+
config.hotkeys, profile.hotkeys, "take_screenshot")
|
864
|
+
self.sync_shared_field(
|
865
|
+
config.hotkeys, profile.hotkeys, "open_utility")
|
866
|
+
self.sync_shared_field(
|
867
|
+
config.hotkeys, profile.hotkeys, "play_latest_audio")
|
868
|
+
self.sync_shared_field(config.anki, profile.anki, "url")
|
869
|
+
self.sync_shared_field(config.anki, profile.anki, "sentence_field")
|
870
|
+
self.sync_shared_field(
|
871
|
+
config.anki, profile.anki, "sentence_audio_field")
|
872
|
+
self.sync_shared_field(config.anki, profile.anki, "picture_field")
|
873
|
+
self.sync_shared_field(config.anki, profile.anki, "word_field")
|
874
|
+
self.sync_shared_field(
|
875
|
+
config.anki, profile.anki, "previous_sentence_field")
|
876
|
+
self.sync_shared_field(
|
877
|
+
config.anki, profile.anki, "previous_image_field")
|
878
|
+
self.sync_shared_field(config.anki, profile.anki, "tags_to_check")
|
879
|
+
self.sync_shared_field(config.anki, profile.anki, "add_game_tag")
|
880
|
+
self.sync_shared_field(config.anki, profile.anki, "polling_rate")
|
881
|
+
self.sync_shared_field(
|
882
|
+
config.anki, profile.anki, "overwrite_audio")
|
883
|
+
self.sync_shared_field(
|
884
|
+
config.anki, profile.anki, "overwrite_picture")
|
885
|
+
self.sync_shared_field(
|
886
|
+
config.anki, profile.anki, "multi_overwrites_sentence")
|
887
|
+
self.sync_shared_field(
|
888
|
+
config.general, profile.general, "open_config_on_startup")
|
889
|
+
self.sync_shared_field(
|
890
|
+
config.general, profile.general, "open_multimine_on_startup")
|
891
|
+
self.sync_shared_field(
|
892
|
+
config.general, profile.general, "websocket_uri")
|
893
|
+
self.sync_shared_field(
|
894
|
+
config.general, profile.general, "texthooker_port")
|
895
|
+
self.sync_shared_field(
|
896
|
+
config.audio, profile.audio, "external_tool")
|
897
|
+
self.sync_shared_field(
|
898
|
+
config.audio, profile.audio, "anki_media_collection")
|
899
|
+
self.sync_shared_field(
|
900
|
+
config.audio, profile.audio, "external_tool_enabled")
|
901
|
+
self.sync_shared_field(
|
902
|
+
config.audio, profile.audio, "custom_encode_settings")
|
903
|
+
self.sync_shared_field(
|
904
|
+
config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
|
905
|
+
self.sync_shared_field(config, profile, "advanced")
|
906
|
+
self.sync_shared_field(config, profile, "paths")
|
907
|
+
self.sync_shared_field(config, profile, "obs")
|
908
|
+
self.sync_shared_field(config, profile, "wip")
|
909
|
+
self.sync_shared_field(config.ai, profile.ai, "anki_field")
|
910
|
+
self.sync_shared_field(config.ai, profile.ai, "provider")
|
911
|
+
self.sync_shared_field(config.ai, profile.ai, "api_key")
|
912
|
+
self.sync_shared_field(config.ai, profile.ai, "gemini_api_key")
|
913
|
+
self.sync_shared_field(config.ai, profile.ai, "groq_api_key")
|
914
|
+
|
915
|
+
return self
|
916
|
+
|
917
|
+
def sync_shared_field(self, config, config2, field_name):
|
918
|
+
try:
|
919
|
+
config_value = getattr(config, field_name, None)
|
920
|
+
config2_value = getattr(config2, field_name, None)
|
921
|
+
|
922
|
+
if config_value != config2_value: # Check if values are different.
|
923
|
+
if config_value is not None:
|
924
|
+
logging.info(
|
925
|
+
f"Syncing shared field '{field_name}' to other profile.")
|
926
|
+
setattr(config2, field_name, config_value)
|
927
|
+
elif config2_value is not None:
|
928
|
+
logging.info(
|
929
|
+
f"Syncing shared field '{field_name}' to current profile.")
|
930
|
+
setattr(config, field_name, config2_value)
|
931
|
+
except AttributeError as e:
|
932
|
+
logging.error(f"AttributeError during sync of '{field_name}': {e}")
|
933
|
+
except Exception as e:
|
934
|
+
logging.error(
|
935
|
+
f"An unexpected error occurred during sync of '{field_name}': {e}")
|
936
|
+
|
937
|
+
|
938
|
+
def get_default_anki_path():
|
939
|
+
if platform == 'win32': # Windows
|
940
|
+
base_dir = os.getenv('APPDATA')
|
941
|
+
else: # macOS and Linux
|
942
|
+
base_dir = '~/.local/share/'
|
943
|
+
config_dir = os.path.join(base_dir, 'Anki2')
|
944
|
+
return config_dir
|
945
|
+
|
946
|
+
|
947
|
+
def get_default_anki_media_collection_path():
|
948
|
+
return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
|
949
|
+
|
950
|
+
|
951
|
+
def get_app_directory():
|
952
|
+
if platform == 'win32': # Windows
|
953
|
+
appdata_dir = os.getenv('APPDATA')
|
954
|
+
else: # macOS and Linux
|
955
|
+
appdata_dir = os.path.expanduser('~/.config')
|
956
|
+
config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
|
957
|
+
# Create the directory if it doesn't exist
|
958
|
+
os.makedirs(config_dir, exist_ok=True)
|
959
|
+
return config_dir
|
960
|
+
|
961
|
+
|
962
|
+
def get_log_path():
|
963
|
+
path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
|
964
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
965
|
+
return path
|
966
|
+
|
967
|
+
|
968
|
+
temp_directory = ''
|
969
|
+
|
970
|
+
|
971
|
+
def get_temporary_directory(delete=False):
|
972
|
+
global temp_directory
|
973
|
+
if not temp_directory:
|
974
|
+
temp_directory = os.path.join(get_app_directory(), 'temp')
|
975
|
+
os.makedirs(temp_directory, exist_ok=True)
|
976
|
+
if delete:
|
977
|
+
for filename in os.listdir(temp_directory):
|
978
|
+
file_path = os.path.join(temp_directory, filename)
|
979
|
+
try:
|
980
|
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
981
|
+
os.unlink(file_path)
|
982
|
+
elif os.path.isdir(file_path):
|
983
|
+
shutil.rmtree(file_path)
|
984
|
+
except Exception as e:
|
985
|
+
logger.error(f"Failed to delete {file_path}. Reason: {e}")
|
986
|
+
return temp_directory
|
987
|
+
|
988
|
+
|
989
|
+
def get_config_path():
|
990
|
+
return os.path.join(get_app_directory(), 'config.json')
|
991
|
+
|
992
|
+
|
993
|
+
def load_config():
|
994
|
+
config_path = get_config_path()
|
995
|
+
|
996
|
+
if os.path.exists('config.json') and not os.path.exists(config_path):
|
997
|
+
shutil.copy('config.json', config_path)
|
998
|
+
|
999
|
+
if os.path.exists(config_path):
|
1000
|
+
try:
|
1001
|
+
with open(config_path, 'r') as file:
|
1002
|
+
config_file = json.load(file)
|
1003
|
+
if "current_profile" in config_file:
|
1004
|
+
return Config.from_dict(config_file)
|
1005
|
+
else:
|
1006
|
+
print(f"Loading Profile-less Config, Converting to new Config!")
|
1007
|
+
with open(config_path, 'r') as file:
|
1008
|
+
config_file = json.load(file)
|
1009
|
+
|
1010
|
+
config = ProfileConfig.from_dict(config_file)
|
1011
|
+
new_config = Config(
|
1012
|
+
configs={DEFAULT_CONFIG: config}, current_profile=DEFAULT_CONFIG)
|
1013
|
+
|
1014
|
+
config.save()
|
1015
|
+
return new_config
|
1016
|
+
except json.JSONDecodeError as e:
|
1017
|
+
logger.error(
|
1018
|
+
f"Error parsing config.json, saving backup and returning new config: {e}")
|
1019
|
+
shutil.copy(config_path, config_path + '.bak')
|
1020
|
+
config = Config.new()
|
1021
|
+
config.save()
|
1022
|
+
return config
|
1023
|
+
elif os.path.exists('config.toml'):
|
1024
|
+
config = ProfileConfig().load_from_toml('config.toml')
|
1025
|
+
new_config = Config({DEFAULT_CONFIG: config},
|
1026
|
+
current_profile=DEFAULT_CONFIG)
|
1027
|
+
return new_config
|
1028
|
+
else:
|
1029
|
+
config = Config.new()
|
1030
|
+
config.save()
|
1031
|
+
return config
|
1032
|
+
|
1033
|
+
|
1034
|
+
config_instance: Config = None
|
1035
|
+
|
1036
|
+
|
1037
|
+
def get_config():
|
1038
|
+
global config_instance
|
1039
|
+
if config_instance is None:
|
1040
|
+
config_instance = load_config()
|
1041
|
+
config = config_instance.get_config()
|
1042
|
+
|
1043
|
+
if config.features.backfill_audio and config.features.full_auto:
|
1044
|
+
logger.warning(
|
1045
|
+
"Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
|
1046
|
+
config.features.backfill_audio = False
|
1047
|
+
|
1048
|
+
# print(config_instance.get_config())
|
1049
|
+
return config_instance.get_config()
|
1050
|
+
|
1051
|
+
|
1052
|
+
def reload_config():
|
1053
|
+
global config_instance
|
1054
|
+
config_instance = load_config()
|
1055
|
+
config = config_instance.get_config()
|
1056
|
+
|
1057
|
+
if config.features.backfill_audio and config.features.full_auto:
|
1058
|
+
logger.warning(
|
1059
|
+
"Backfill is enabled, but full auto is also enabled. Disabling backfill...")
|
1060
|
+
config.features.backfill_audio = False
|
1061
|
+
|
1062
|
+
|
1063
|
+
def get_master_config():
|
1064
|
+
return config_instance
|
1065
|
+
|
1066
|
+
|
1067
|
+
def save_full_config(config):
|
1068
|
+
with open(get_config_path(), 'w') as file:
|
1069
|
+
json.dump(config.to_dict(), file, indent=4)
|
1070
|
+
|
1071
|
+
|
1072
|
+
def save_current_config(config):
|
1073
|
+
global config_instance
|
1074
|
+
config_instance.set_config_for_profile(
|
1075
|
+
config_instance.current_profile, config)
|
1076
|
+
save_full_config(config_instance)
|
1077
|
+
|
1078
|
+
|
1079
|
+
def switch_profile_and_save(profile_name):
|
1080
|
+
global config_instance
|
1081
|
+
config_instance.current_profile = profile_name
|
1082
|
+
save_full_config(config_instance)
|
1083
|
+
return config_instance.get_config()
|
1084
|
+
|
1085
|
+
|
1086
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
1087
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
1088
|
+
|
1089
|
+
logger = logging.getLogger("GameSentenceMiner")
|
1090
|
+
# Set the base level to DEBUG so that all messages are captured
|
1091
|
+
logger.setLevel(logging.DEBUG)
|
1092
|
+
formatter = logging.Formatter(
|
1093
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
1094
|
+
|
1095
|
+
# Create console handler with level INFO
|
1096
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
1097
|
+
console_handler.setLevel(logging.INFO)
|
1098
|
+
|
1099
|
+
console_handler.setFormatter(formatter)
|
1100
|
+
|
1101
|
+
logger.addHandler(console_handler)
|
1102
|
+
|
1103
|
+
file_path = get_log_path()
|
1104
|
+
try:
|
1105
|
+
if os.path.exists(file_path) and os.path.getsize(file_path) > 1 * 1024 * 1024 and os.access(file_path, os.W_OK):
|
1106
|
+
old_log_path = os.path.join(os.path.dirname(
|
1107
|
+
file_path), "gamesentenceminer_old.log")
|
1108
|
+
if os.path.exists(old_log_path):
|
1109
|
+
os.remove(old_log_path)
|
1110
|
+
shutil.move(file_path, old_log_path)
|
1111
|
+
except Exception as e:
|
1112
|
+
logger.info(
|
1113
|
+
"Couldn't rotate log, probably because the file is being written to by another process. NOT AN ERROR")
|
1114
|
+
|
1115
|
+
file_handler = logging.FileHandler(file_path, encoding='utf-8')
|
1116
|
+
file_handler.setLevel(logging.DEBUG)
|
1117
|
+
file_handler.setFormatter(formatter)
|
1118
|
+
logger.addHandler(file_handler)
|
1119
|
+
|
1120
|
+
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
1121
|
+
|
1122
|
+
|
1123
|
+
class GsmAppState:
|
1124
|
+
def __init__(self):
|
1125
|
+
self.line_for_audio = None
|
1126
|
+
self.line_for_screenshot = None
|
1127
|
+
self.anki_note_for_screenshot = None
|
1128
|
+
self.previous_line_for_audio = None
|
1129
|
+
self.previous_line_for_screenshot = None
|
1130
|
+
self.previous_trim_args = None
|
1131
|
+
self.previous_audio = None
|
1132
|
+
self.previous_screenshot = None
|
1133
|
+
self.previous_replay = None
|
1134
|
+
self.lock = threading.Lock()
|
1135
|
+
self.last_mined_line = None
|
1136
|
+
self.keep_running = True
|
1137
|
+
self.current_game = ''
|
1138
|
+
self.videos_to_remove = set()
|
1139
|
+
|
1140
|
+
|
1141
|
+
@dataclass_json
|
1142
|
+
@dataclass
|
1143
|
+
class AnkiUpdateResult:
|
1144
|
+
success: bool = False
|
1145
|
+
audio_in_anki: str = ''
|
1146
|
+
screenshot_in_anki: str = ''
|
1147
|
+
prev_screenshot_in_anki: str = ''
|
1148
|
+
sentence_in_anki: str = ''
|
1149
|
+
multi_line: bool = False
|
1150
|
+
|
1151
|
+
@staticmethod
|
1152
|
+
def failure():
|
1153
|
+
return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False)
|
1154
|
+
|
1155
|
+
|
1156
|
+
@dataclass_json
|
1157
|
+
@dataclass
|
1158
|
+
class GsmStatus:
|
1159
|
+
ready: bool = False
|
1160
|
+
status: bool = "Initializing"
|
1161
|
+
cards_created: int = 0
|
1162
|
+
websockets_connected: List[str] = field(default_factory=list)
|
1163
|
+
obs_connected: bool = False
|
1164
|
+
anki_connected: bool = False
|
1165
|
+
last_line_received: str = None
|
1166
|
+
words_being_processed: List[str] = field(default_factory=list)
|
1167
|
+
clipboard_enabled: bool = True
|
1168
|
+
|
1169
|
+
def add_word_being_processed(self, word: str):
|
1170
|
+
if word not in self.words_being_processed:
|
1171
|
+
self.words_being_processed.append(word)
|
1172
|
+
|
1173
|
+
def remove_word_being_processed(self, word: str):
|
1174
|
+
if word in self.words_being_processed:
|
1175
|
+
self.words_being_processed.remove(word)
|
1176
|
+
|
1177
|
+
|
1178
|
+
def is_running_from_source():
|
1179
|
+
# Check for .git directory at the project root
|
1180
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
1181
|
+
project_root = current_dir
|
1182
|
+
while project_root != os.path.dirname(project_root): # Avoid infinite loop
|
1183
|
+
if os.path.isdir(os.path.join(project_root, '.git')):
|
1184
|
+
return True
|
1185
|
+
if os.path.isfile(os.path.join(project_root, 'pyproject.toml')):
|
1186
|
+
return True
|
1187
|
+
project_root = os.path.dirname(project_root)
|
1188
|
+
return False
|
1189
|
+
|
1190
|
+
|
1191
|
+
gsm_status = GsmStatus()
|
1192
|
+
anki_results = {}
|
1193
|
+
gsm_state = GsmAppState()
|
1194
|
+
is_dev = is_running_from_source()
|
1195
|
+
|
1196
|
+
is_beangate = os.path.exists("C:/Users/Beangate")
|
1197
|
+
|
1198
|
+
logger.debug(f"Running in development mode: {is_dev}")
|
1199
|
+
logger.debug(f"Running on Beangate's PC: {is_beangate}")
|