GameSentenceMiner 2.9.5__py3-none-any.whl → 2.9.7__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/gsm.py +0 -1
- GameSentenceMiner/obs.py +2 -4
- GameSentenceMiner/util/__init__.py +0 -0
- GameSentenceMiner/util/configuration.py +653 -0
- GameSentenceMiner/util/electron_config.py +315 -0
- GameSentenceMiner/util/ffmpeg.py +449 -0
- GameSentenceMiner/util/gsm_utils.py +235 -0
- GameSentenceMiner/util/model.py +177 -0
- GameSentenceMiner/util/notification.py +124 -0
- GameSentenceMiner/util/package.py +37 -0
- GameSentenceMiner/util/ss_selector.py +122 -0
- GameSentenceMiner/util/text_log.py +186 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/RECORD +18 -8
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/WHEEL +1 -1
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.7.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gsm.py
CHANGED
GameSentenceMiner/obs.py
CHANGED
@@ -313,8 +313,7 @@ async def register_scene_change_callback(callback):
|
|
313
313
|
|
314
314
|
def get_screenshot(compression=-1):
|
315
315
|
try:
|
316
|
-
screenshot =
|
317
|
-
configuration.get_temporary_directory()) + '/screenshot.png')
|
316
|
+
screenshot = os.path.join(configuration.get_temporary_directory(), make_unique_file_name('screenshot.png'))
|
318
317
|
update_current_game()
|
319
318
|
if not configuration.current_game:
|
320
319
|
logger.error("No active game scene found.")
|
@@ -326,8 +325,7 @@ def get_screenshot(compression=-1):
|
|
326
325
|
return None
|
327
326
|
start = time.time()
|
328
327
|
logger.debug(f"Current source name: {current_source_name}")
|
329
|
-
|
330
|
-
logger.debug(f"Screenshot response: {response}")
|
328
|
+
client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
|
331
329
|
logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
|
332
330
|
return screenshot
|
333
331
|
except Exception as e:
|
File without changes
|
@@ -0,0 +1,653 @@
|
|
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
|
+
OFF = 'OFF'
|
19
|
+
VOSK = 'VOSK'
|
20
|
+
SILERO = 'SILERO'
|
21
|
+
WHISPER = 'WHISPER'
|
22
|
+
GROQ = 'GROQ'
|
23
|
+
|
24
|
+
VOSK_BASE = 'BASE'
|
25
|
+
VOSK_SMALL = 'SMALL'
|
26
|
+
|
27
|
+
WHISPER_TINY = 'tiny'
|
28
|
+
WHISPER_BASE = 'base'
|
29
|
+
WHISPER_SMALL = 'small'
|
30
|
+
WHISPER_MEDIUM = 'medium'
|
31
|
+
WHSIPER_LARGE = 'large'
|
32
|
+
|
33
|
+
AI_GEMINI = 'Gemini'
|
34
|
+
AI_GROQ = 'Groq'
|
35
|
+
|
36
|
+
INFO = 'INFO'
|
37
|
+
DEBUG = 'DEBUG'
|
38
|
+
|
39
|
+
DEFAULT_CONFIG = 'Default'
|
40
|
+
|
41
|
+
current_game = ''
|
42
|
+
|
43
|
+
def is_linux():
|
44
|
+
return platform == 'linux'
|
45
|
+
|
46
|
+
|
47
|
+
def is_windows():
|
48
|
+
return platform == 'win32'
|
49
|
+
|
50
|
+
|
51
|
+
class Language(Enum):
|
52
|
+
JAPANESE = "ja"
|
53
|
+
ENGLISH = "en"
|
54
|
+
KOREAN = "ko"
|
55
|
+
CHINESE = "zh"
|
56
|
+
SPANISH = "es"
|
57
|
+
FRENCH = "fr"
|
58
|
+
GERMAN = "de"
|
59
|
+
ITALIAN = "it"
|
60
|
+
RUSSIAN = "ru"
|
61
|
+
PORTUGUESE = "pt"
|
62
|
+
HINDI = "hi"
|
63
|
+
ARABIC = "ar"
|
64
|
+
|
65
|
+
AVAILABLE_LANGUAGES = [lang.value for lang in Language]
|
66
|
+
AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
|
67
|
+
|
68
|
+
@dataclass_json
|
69
|
+
@dataclass
|
70
|
+
class General:
|
71
|
+
use_websocket: bool = True
|
72
|
+
use_clipboard: bool = True
|
73
|
+
use_both_clipboard_and_websocket: bool = False
|
74
|
+
websocket_uri: str = 'localhost:6677,localhost:9001,localhost:2333'
|
75
|
+
open_config_on_startup: bool = False
|
76
|
+
open_multimine_on_startup: bool = True
|
77
|
+
texthook_replacement_regex: str = ""
|
78
|
+
texthooker_port: int = 55000
|
79
|
+
use_old_texthooker: bool = False
|
80
|
+
|
81
|
+
|
82
|
+
@dataclass_json
|
83
|
+
@dataclass
|
84
|
+
class Paths:
|
85
|
+
folder_to_watch: str = expanduser("~/Videos/GSM")
|
86
|
+
audio_destination: str = expanduser("~/Videos/GSM/Audio/")
|
87
|
+
screenshot_destination: str = expanduser("~/Videos/GSM/SS/")
|
88
|
+
remove_video: bool = True
|
89
|
+
remove_audio: bool = False
|
90
|
+
remove_screenshot: bool = False
|
91
|
+
|
92
|
+
|
93
|
+
@dataclass_json
|
94
|
+
@dataclass
|
95
|
+
class Anki:
|
96
|
+
update_anki: bool = True
|
97
|
+
url: str = 'http://127.0.0.1:8765'
|
98
|
+
sentence_field: str = "Sentence"
|
99
|
+
sentence_audio_field: str = "SentenceAudio"
|
100
|
+
picture_field: str = "Picture"
|
101
|
+
word_field: str = 'Expression'
|
102
|
+
previous_sentence_field: str = ''
|
103
|
+
previous_image_field: str = ''
|
104
|
+
custom_tags: List[str] = None # Initialize to None and set it in __post_init__
|
105
|
+
tags_to_check: List[str] = None
|
106
|
+
add_game_tag: bool = True
|
107
|
+
polling_rate: int = 200
|
108
|
+
overwrite_audio: bool = False
|
109
|
+
overwrite_picture: bool = True
|
110
|
+
multi_overwrites_sentence: bool = True
|
111
|
+
anki_custom_fields: Dict[str, str] = None # Initialize to None and set it in __post_init__
|
112
|
+
|
113
|
+
def __post_init__(self):
|
114
|
+
if self.custom_tags is None:
|
115
|
+
self.custom_tags = []
|
116
|
+
if self.anki_custom_fields is None:
|
117
|
+
self.anki_custom_fields = {}
|
118
|
+
if self.tags_to_check is None:
|
119
|
+
self.tags_to_check = []
|
120
|
+
|
121
|
+
|
122
|
+
@dataclass_json
|
123
|
+
@dataclass
|
124
|
+
class Features:
|
125
|
+
full_auto: bool = True
|
126
|
+
notify_on_update: bool = True
|
127
|
+
open_anki_edit: bool = False
|
128
|
+
open_anki_in_browser: bool = True
|
129
|
+
browser_query: str = ''
|
130
|
+
backfill_audio: bool = False
|
131
|
+
|
132
|
+
|
133
|
+
@dataclass_json
|
134
|
+
@dataclass
|
135
|
+
class Screenshot:
|
136
|
+
enabled: bool = True
|
137
|
+
width: str = 0
|
138
|
+
height: str = 0
|
139
|
+
quality: str = 85
|
140
|
+
extension: str = "webp"
|
141
|
+
custom_ffmpeg_settings: str = ''
|
142
|
+
custom_ffmpeg_option_selected: str = ''
|
143
|
+
screenshot_hotkey_updates_anki: bool = False
|
144
|
+
seconds_after_line: float = 1.0
|
145
|
+
use_beginning_of_line_as_screenshot: bool = True
|
146
|
+
use_new_screenshot_logic: bool = False
|
147
|
+
screenshot_timing_setting: str = '' # 'middle', 'end'
|
148
|
+
|
149
|
+
def __post_init__(self):
|
150
|
+
if not self.screenshot_timing_setting and self.use_beginning_of_line_as_screenshot:
|
151
|
+
self.screenshot_timing_setting = 'beginning'
|
152
|
+
if not self.screenshot_timing_setting and self.use_new_screenshot_logic:
|
153
|
+
self.screenshot_timing_setting = 'middle'
|
154
|
+
if not self.screenshot_timing_setting and not self.use_beginning_of_line_as_screenshot and not self.use_new_screenshot_logic:
|
155
|
+
self.screenshot_timing_setting = 'end'
|
156
|
+
|
157
|
+
|
158
|
+
|
159
|
+
@dataclass_json
|
160
|
+
@dataclass
|
161
|
+
class Audio:
|
162
|
+
enabled: bool = True
|
163
|
+
extension: str = 'opus'
|
164
|
+
beginning_offset: float = 0.0
|
165
|
+
end_offset: float = 0.5
|
166
|
+
ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
|
167
|
+
external_tool: str = ""
|
168
|
+
anki_media_collection: str = ""
|
169
|
+
external_tool_enabled: bool = True
|
170
|
+
custom_encode_settings: str = ''
|
171
|
+
|
172
|
+
|
173
|
+
@dataclass_json
|
174
|
+
@dataclass
|
175
|
+
class OBS:
|
176
|
+
enabled: bool = True
|
177
|
+
open_obs: bool = True
|
178
|
+
close_obs: bool = True
|
179
|
+
host: str = "127.0.0.1"
|
180
|
+
port: int = 7274
|
181
|
+
password: str = "your_password"
|
182
|
+
get_game_from_scene: bool = True
|
183
|
+
minimum_replay_size: int = 0
|
184
|
+
|
185
|
+
|
186
|
+
@dataclass_json
|
187
|
+
@dataclass
|
188
|
+
class Hotkeys:
|
189
|
+
reset_line: str = 'f5'
|
190
|
+
take_screenshot: str = 'f6'
|
191
|
+
open_utility: str = 'ctrl+m'
|
192
|
+
play_latest_audio: str = 'f7'
|
193
|
+
|
194
|
+
|
195
|
+
@dataclass_json
|
196
|
+
@dataclass
|
197
|
+
class VAD:
|
198
|
+
whisper_model: str = WHISPER_BASE
|
199
|
+
do_vad_postprocessing: bool = True
|
200
|
+
language: str = 'ja'
|
201
|
+
vosk_url: str = VOSK_BASE
|
202
|
+
selected_vad_model: str = SILERO
|
203
|
+
backup_vad_model: str = OFF
|
204
|
+
trim_beginning: bool = False
|
205
|
+
beginning_offset: float = -0.25
|
206
|
+
add_audio_on_no_results: bool = False
|
207
|
+
|
208
|
+
def is_silero(self):
|
209
|
+
return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
|
210
|
+
|
211
|
+
def is_whisper(self):
|
212
|
+
return self.selected_vad_model == WHISPER or self.backup_vad_model == WHISPER
|
213
|
+
|
214
|
+
def is_vosk(self):
|
215
|
+
return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
|
216
|
+
|
217
|
+
def is_groq(self):
|
218
|
+
return self.selected_vad_model == GROQ or self.backup_vad_model == GROQ
|
219
|
+
|
220
|
+
|
221
|
+
@dataclass_json
|
222
|
+
@dataclass
|
223
|
+
class Advanced:
|
224
|
+
audio_player_path: str = ''
|
225
|
+
video_player_path: str = ''
|
226
|
+
show_screenshot_buttons: bool = False
|
227
|
+
multi_line_line_break: str = '<br>'
|
228
|
+
multi_line_sentence_storage_field: str = ''
|
229
|
+
ocr_sends_to_clipboard: bool = True
|
230
|
+
ocr_websocket_port: int = 9002
|
231
|
+
texthooker_communication_websocket_port: int = 55001
|
232
|
+
use_anki_note_creation_time: bool = False
|
233
|
+
|
234
|
+
@dataclass_json
|
235
|
+
@dataclass
|
236
|
+
class Ai:
|
237
|
+
enabled: bool = False
|
238
|
+
anki_field: str = ''
|
239
|
+
provider: str = AI_GEMINI
|
240
|
+
gemini_model: str = 'gemini-2.0-flash'
|
241
|
+
groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
242
|
+
api_key: str = '' # Deprecated
|
243
|
+
gemini_api_key: str = ''
|
244
|
+
groq_api_key: str = ''
|
245
|
+
use_canned_translation_prompt: bool = True
|
246
|
+
use_canned_context_prompt: bool = False
|
247
|
+
custom_prompt: str = ''
|
248
|
+
|
249
|
+
def __post_init__(self):
|
250
|
+
if not self.gemini_api_key:
|
251
|
+
self.gemini_api_key = self.api_key
|
252
|
+
if self.provider == 'gemini':
|
253
|
+
self.provider = AI_GEMINI
|
254
|
+
if self.provider == 'groq':
|
255
|
+
self.provider = AI_GROQ
|
256
|
+
|
257
|
+
@dataclass_json
|
258
|
+
@dataclass
|
259
|
+
class ProfileConfig:
|
260
|
+
name: str = 'Default'
|
261
|
+
scenes: List[str] = field(default_factory=list)
|
262
|
+
general: General = field(default_factory=General)
|
263
|
+
paths: Paths = field(default_factory=Paths)
|
264
|
+
anki: Anki = field(default_factory=Anki)
|
265
|
+
features: Features = field(default_factory=Features)
|
266
|
+
screenshot: Screenshot = field(default_factory=Screenshot)
|
267
|
+
audio: Audio = field(default_factory=Audio)
|
268
|
+
obs: OBS = field(default_factory=OBS)
|
269
|
+
hotkeys: Hotkeys = field(default_factory=Hotkeys)
|
270
|
+
vad: VAD = field(default_factory=VAD)
|
271
|
+
advanced: Advanced = field(default_factory=Advanced)
|
272
|
+
ai: Ai = field(default_factory=Ai)
|
273
|
+
|
274
|
+
|
275
|
+
# This is just for legacy support
|
276
|
+
def load_from_toml(self, file_path: str):
|
277
|
+
with open(file_path, 'r') as f:
|
278
|
+
config_data = toml.load(f)
|
279
|
+
|
280
|
+
self.paths.folder_to_watch = expanduser(config_data['paths'].get('folder_to_watch', self.paths.folder_to_watch))
|
281
|
+
self.paths.audio_destination = expanduser(
|
282
|
+
config_data['paths'].get('audio_destination', self.paths.audio_destination))
|
283
|
+
self.paths.screenshot_destination = expanduser(config_data['paths'].get('screenshot_destination',
|
284
|
+
self.paths.screenshot_destination))
|
285
|
+
|
286
|
+
self.anki.url = config_data['anki'].get('url', self.anki.url)
|
287
|
+
self.anki.sentence_field = config_data['anki'].get('sentence_field', self.anki.sentence_field)
|
288
|
+
self.anki.sentence_audio_field = config_data['anki'].get('sentence_audio_field', self.anki.sentence_audio_field)
|
289
|
+
self.anki.word_field = config_data['anki'].get('word_field', self.anki.word_field)
|
290
|
+
self.anki.picture_field = config_data['anki'].get('picture_field', self.anki.picture_field)
|
291
|
+
self.anki.custom_tags = config_data['anki'].get('custom_tags', self.anki.custom_tags)
|
292
|
+
self.anki.add_game_tag = config_data['anki'].get('add_game_tag', self.anki.add_game_tag)
|
293
|
+
self.anki.polling_rate = config_data['anki'].get('polling_rate', self.anki.polling_rate)
|
294
|
+
self.anki.overwrite_audio = config_data['anki_overwrites'].get('overwrite_audio', self.anki.overwrite_audio)
|
295
|
+
self.anki.overwrite_picture = config_data['anki_overwrites'].get('overwrite_picture',
|
296
|
+
self.anki.overwrite_picture)
|
297
|
+
|
298
|
+
self.features.full_auto = config_data['features'].get('do_vosk_postprocessing', self.features.full_auto)
|
299
|
+
self.features.notify_on_update = config_data['features'].get('notify_on_update', self.features.notify_on_update)
|
300
|
+
self.features.open_anki_edit = config_data['features'].get('open_anki_edit', self.features.open_anki_edit)
|
301
|
+
self.features.backfill_audio = config_data['features'].get('backfill_audio', self.features.backfill_audio)
|
302
|
+
|
303
|
+
self.screenshot.width = config_data['screenshot'].get('width', self.screenshot.width)
|
304
|
+
self.screenshot.height = config_data['screenshot'].get('height', self.screenshot.height)
|
305
|
+
self.screenshot.quality = config_data['screenshot'].get('quality', self.screenshot.quality)
|
306
|
+
self.screenshot.extension = config_data['screenshot'].get('extension', self.screenshot.extension)
|
307
|
+
self.screenshot.custom_ffmpeg_settings = config_data['screenshot'].get('custom_ffmpeg_settings',
|
308
|
+
self.screenshot.custom_ffmpeg_settings)
|
309
|
+
|
310
|
+
self.audio.extension = config_data['audio'].get('extension', self.audio.extension)
|
311
|
+
self.audio.beginning_offset = config_data['audio'].get('beginning_offset', self.audio.beginning_offset)
|
312
|
+
self.audio.end_offset = config_data['audio'].get('end_offset', self.audio.end_offset)
|
313
|
+
self.audio.ffmpeg_reencode_options = config_data['audio'].get('ffmpeg_reencode_options',
|
314
|
+
self.audio.ffmpeg_reencode_options)
|
315
|
+
|
316
|
+
self.vad.whisper_model = config_data['vosk'].get('whisper_model', self.vad.whisper_model)
|
317
|
+
self.vad.vosk_url = config_data['vosk'].get('url', self.vad.vosk_url)
|
318
|
+
self.vad.do_vad_postprocessing = config_data['features'].get('do_vosk_postprocessing',
|
319
|
+
self.vad.do_vad_postprocessing)
|
320
|
+
self.vad.trim_beginning = config_data['audio'].get('vosk_trim_beginning', self.vad.trim_beginning)
|
321
|
+
|
322
|
+
self.obs.enabled = config_data['obs'].get('enabled', self.obs.enabled)
|
323
|
+
self.obs.host = config_data['obs'].get('host', self.obs.host)
|
324
|
+
self.obs.port = config_data['obs'].get('port', self.obs.port)
|
325
|
+
self.obs.password = config_data['obs'].get('password', self.obs.password)
|
326
|
+
|
327
|
+
self.general.use_websocket = config_data['websocket'].get('enabled', self.general.use_websocket)
|
328
|
+
self.general.websocket_uri = config_data['websocket'].get('uri', self.general.websocket_uri)
|
329
|
+
|
330
|
+
self.hotkeys.reset_line = config_data['hotkeys'].get('reset_line', self.hotkeys.reset_line)
|
331
|
+
self.hotkeys.take_screenshot = config_data['hotkeys'].get('take_screenshot', self.hotkeys.take_screenshot)
|
332
|
+
|
333
|
+
self.anki.anki_custom_fields = config_data.get('anki_custom_fields', {})
|
334
|
+
|
335
|
+
with open(get_config_path(), 'w') as f:
|
336
|
+
f.write(self.to_json(indent=4))
|
337
|
+
print(
|
338
|
+
'config.json successfully generated from previous settings. config.toml will no longer be used.')
|
339
|
+
|
340
|
+
return self
|
341
|
+
|
342
|
+
def restart_required(self, previous):
|
343
|
+
previous: ProfileConfig
|
344
|
+
if any([previous.paths.folder_to_watch != self.paths.folder_to_watch,
|
345
|
+
previous.obs.open_obs != self.obs.open_obs,
|
346
|
+
previous.obs.host != self.obs.host,
|
347
|
+
previous.obs.port != self.obs.port
|
348
|
+
]):
|
349
|
+
logger.info("Restart Required for Some Settings that were Changed")
|
350
|
+
return True
|
351
|
+
return False
|
352
|
+
|
353
|
+
def config_changed(self, new: 'ProfileConfig') -> bool:
|
354
|
+
return self != new
|
355
|
+
|
356
|
+
|
357
|
+
@dataclass_json
|
358
|
+
@dataclass
|
359
|
+
class Config:
|
360
|
+
configs: Dict[str, ProfileConfig] = field(default_factory=dict)
|
361
|
+
current_profile: str = DEFAULT_CONFIG
|
362
|
+
switch_to_default_if_not_found: bool = True
|
363
|
+
|
364
|
+
@classmethod
|
365
|
+
def new(cls):
|
366
|
+
instance = cls(configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
|
367
|
+
return instance
|
368
|
+
|
369
|
+
@classmethod
|
370
|
+
def load(cls):
|
371
|
+
config_path = get_config_path()
|
372
|
+
if os.path.exists(config_path):
|
373
|
+
with open(config_path, 'r') as file:
|
374
|
+
data = json.load(file)
|
375
|
+
return cls.from_dict(data)
|
376
|
+
else:
|
377
|
+
return cls.new()
|
378
|
+
|
379
|
+
def save(self):
|
380
|
+
with open(get_config_path(), 'w') as file:
|
381
|
+
json.dump(self.to_dict(), file, indent=4)
|
382
|
+
return self
|
383
|
+
|
384
|
+
def get_config(self) -> ProfileConfig:
|
385
|
+
if self.current_profile not in self.configs:
|
386
|
+
logger.warning(f"Profile '{self.current_profile}' not found. Switching to default profile.")
|
387
|
+
self.current_profile = DEFAULT_CONFIG
|
388
|
+
return self.configs[self.current_profile]
|
389
|
+
|
390
|
+
def set_config_for_profile(self, profile: str, config: ProfileConfig):
|
391
|
+
config.name = profile
|
392
|
+
self.configs[profile] = config
|
393
|
+
|
394
|
+
def has_config_for_current_game(self):
|
395
|
+
return current_game in self.configs
|
396
|
+
|
397
|
+
def get_all_profile_names(self):
|
398
|
+
return list(self.configs.keys())
|
399
|
+
|
400
|
+
def get_default_config(self):
|
401
|
+
return self.configs[DEFAULT_CONFIG]
|
402
|
+
|
403
|
+
def sync_changed_fields(self, previous_config: ProfileConfig):
|
404
|
+
current_config = self.get_config()
|
405
|
+
|
406
|
+
for section in current_config.to_dict():
|
407
|
+
if dataclasses.is_dataclass(getattr(current_config, section, None)):
|
408
|
+
for field_name in getattr(current_config, section, None).to_dict():
|
409
|
+
config_section = getattr(current_config, section, None)
|
410
|
+
previous_config_section = getattr(previous_config, section, None)
|
411
|
+
current_value = getattr(config_section, field_name, None)
|
412
|
+
previous_value = getattr(previous_config_section, field_name, None)
|
413
|
+
if str(current_value).strip() != str(previous_value).strip():
|
414
|
+
logger.info(f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
|
415
|
+
for profile in self.configs.values():
|
416
|
+
if profile != current_config:
|
417
|
+
profile_section = getattr(profile, section, None)
|
418
|
+
if profile_section:
|
419
|
+
setattr(profile_section, field_name, current_value)
|
420
|
+
logger.info(f"Updated '{field_name}' in profile '{profile.name}'")
|
421
|
+
|
422
|
+
return self
|
423
|
+
|
424
|
+
def sync_shared_fields(self):
|
425
|
+
config = self.get_config()
|
426
|
+
for profile in self.configs.values():
|
427
|
+
self.sync_shared_field(config.hotkeys, profile.hotkeys, "reset_line")
|
428
|
+
self.sync_shared_field(config.hotkeys, profile.hotkeys, "take_screenshot")
|
429
|
+
self.sync_shared_field(config.hotkeys, profile.hotkeys, "open_utility")
|
430
|
+
self.sync_shared_field(config.hotkeys, profile.hotkeys, "play_latest_audio")
|
431
|
+
self.sync_shared_field(config.anki, profile.anki, "url")
|
432
|
+
self.sync_shared_field(config.anki, profile.anki, "sentence_field")
|
433
|
+
self.sync_shared_field(config.anki, profile.anki, "sentence_audio_field")
|
434
|
+
self.sync_shared_field(config.anki, profile.anki, "picture_field")
|
435
|
+
self.sync_shared_field(config.anki, profile.anki, "word_field")
|
436
|
+
self.sync_shared_field(config.anki, profile.anki, "previous_sentence_field")
|
437
|
+
self.sync_shared_field(config.anki, profile.anki, "previous_image_field")
|
438
|
+
self.sync_shared_field(config.anki, profile.anki, "tags_to_check")
|
439
|
+
self.sync_shared_field(config.anki, profile.anki, "add_game_tag")
|
440
|
+
self.sync_shared_field(config.anki, profile.anki, "polling_rate")
|
441
|
+
self.sync_shared_field(config.anki, profile.anki, "overwrite_audio")
|
442
|
+
self.sync_shared_field(config.anki, profile.anki, "overwrite_picture")
|
443
|
+
self.sync_shared_field(config.anki, profile.anki, "multi_overwrites_sentence")
|
444
|
+
self.sync_shared_field(config.anki, profile.anki, "anki_custom_fields")
|
445
|
+
self.sync_shared_field(config.general, profile.general, "open_config_on_startup")
|
446
|
+
self.sync_shared_field(config.general, profile.general, "open_multimine_on_startup")
|
447
|
+
self.sync_shared_field(config.general, profile.general, "websocket_uri")
|
448
|
+
self.sync_shared_field(config.general, profile.general, "texthooker_port")
|
449
|
+
self.sync_shared_field(config.general, profile.general, "use_old_texthooker")
|
450
|
+
self.sync_shared_field(config.audio, profile.audio, "external_tool")
|
451
|
+
self.sync_shared_field(config.audio, profile.audio, "anki_media_collection")
|
452
|
+
self.sync_shared_field(config.audio, profile.audio, "external_tool_enabled")
|
453
|
+
self.sync_shared_field(config.audio, profile.audio, "custom_encode_settings")
|
454
|
+
self.sync_shared_field(config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
|
455
|
+
self.sync_shared_field(config, profile, "advanced")
|
456
|
+
self.sync_shared_field(config, profile, "paths")
|
457
|
+
self.sync_shared_field(config, profile, "obs")
|
458
|
+
self.sync_shared_field(config.ai, profile.ai, "anki_field")
|
459
|
+
self.sync_shared_field(config.ai, profile.ai, "provider")
|
460
|
+
self.sync_shared_field(config.ai, profile.ai, "api_key")
|
461
|
+
self.sync_shared_field(config.ai, profile.ai, "gemini_api_key")
|
462
|
+
self.sync_shared_field(config.ai, profile.ai, "groq_api_key")
|
463
|
+
|
464
|
+
return self
|
465
|
+
|
466
|
+
|
467
|
+
def sync_shared_field(self, config, config2, field_name):
|
468
|
+
try:
|
469
|
+
config_value = getattr(config, field_name, None)
|
470
|
+
config2_value = getattr(config2, field_name, None)
|
471
|
+
|
472
|
+
if config_value != config2_value: # Check if values are different.
|
473
|
+
if config_value is not None:
|
474
|
+
logging.info(f"Syncing shared field '{field_name}' to other profile.")
|
475
|
+
setattr(config2, field_name, config_value)
|
476
|
+
elif config2_value is not None:
|
477
|
+
logging.info(f"Syncing shared field '{field_name}' to current profile.")
|
478
|
+
setattr(config, field_name, config2_value)
|
479
|
+
except AttributeError as e:
|
480
|
+
logging.error(f"AttributeError during sync of '{field_name}': {e}")
|
481
|
+
except Exception as e:
|
482
|
+
logging.error(f"An unexpected error occurred during sync of '{field_name}': {e}")
|
483
|
+
|
484
|
+
|
485
|
+
def get_default_anki_path():
|
486
|
+
if platform == 'win32': # Windows
|
487
|
+
base_dir = os.getenv('APPDATA')
|
488
|
+
else: # macOS and Linux
|
489
|
+
base_dir = '~/.local/share/'
|
490
|
+
config_dir = os.path.join(base_dir, 'Anki2')
|
491
|
+
return config_dir
|
492
|
+
|
493
|
+
def get_default_anki_media_collection_path():
|
494
|
+
return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
|
495
|
+
|
496
|
+
def get_app_directory():
|
497
|
+
if platform == 'win32': # Windows
|
498
|
+
appdata_dir = os.getenv('APPDATA')
|
499
|
+
else: # macOS and Linux
|
500
|
+
appdata_dir = os.path.expanduser('~/.config')
|
501
|
+
config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
|
502
|
+
os.makedirs(config_dir, exist_ok=True) # Create the directory if it doesn't exist
|
503
|
+
return config_dir
|
504
|
+
|
505
|
+
|
506
|
+
def get_log_path():
|
507
|
+
path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
|
508
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
509
|
+
return path
|
510
|
+
|
511
|
+
temp_directory = ''
|
512
|
+
|
513
|
+
def get_temporary_directory(delete=True):
|
514
|
+
global temp_directory
|
515
|
+
if not temp_directory:
|
516
|
+
temp_directory = os.path.join(get_app_directory(), 'temp')
|
517
|
+
os.makedirs(temp_directory, exist_ok=True)
|
518
|
+
if delete:
|
519
|
+
for filename in os.listdir(temp_directory):
|
520
|
+
file_path = os.path.join(temp_directory, filename)
|
521
|
+
try:
|
522
|
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
523
|
+
os.unlink(file_path)
|
524
|
+
elif os.path.isdir(file_path):
|
525
|
+
shutil.rmtree(file_path)
|
526
|
+
except Exception as e:
|
527
|
+
logger.error(f"Failed to delete {file_path}. Reason: {e}")
|
528
|
+
return temp_directory
|
529
|
+
|
530
|
+
def get_config_path():
|
531
|
+
return os.path.join(get_app_directory(), 'config.json')
|
532
|
+
|
533
|
+
|
534
|
+
def load_config():
|
535
|
+
config_path = get_config_path()
|
536
|
+
|
537
|
+
if os.path.exists('config.json') and not os.path.exists(config_path):
|
538
|
+
shutil.copy('config.json', config_path)
|
539
|
+
|
540
|
+
if os.path.exists(config_path):
|
541
|
+
try:
|
542
|
+
with open(config_path, 'r') as file:
|
543
|
+
config_file = json.load(file)
|
544
|
+
if "current_profile" in config_file:
|
545
|
+
return Config.from_dict(config_file)
|
546
|
+
else:
|
547
|
+
print(f"Loading Profile-less Config, Converting to new Config!")
|
548
|
+
with open(config_path, 'r') as file:
|
549
|
+
config_file = json.load(file)
|
550
|
+
|
551
|
+
config = ProfileConfig.from_dict(config_file)
|
552
|
+
new_config = Config(configs = {DEFAULT_CONFIG : config}, current_profile=DEFAULT_CONFIG)
|
553
|
+
|
554
|
+
with open(config_path, 'w') as file:
|
555
|
+
json.dump(new_config.to_dict(), file, indent=4)
|
556
|
+
return new_config
|
557
|
+
except json.JSONDecodeError as e:
|
558
|
+
logger.error(f"Error parsing config.json: {e}")
|
559
|
+
return None
|
560
|
+
elif os.path.exists('config.toml'):
|
561
|
+
config = ProfileConfig().load_from_toml('config.toml')
|
562
|
+
new_config = Config({DEFAULT_CONFIG: config}, current_profile=DEFAULT_CONFIG)
|
563
|
+
return new_config
|
564
|
+
else:
|
565
|
+
config = Config.new()
|
566
|
+
with open(config_path, 'w') as file:
|
567
|
+
json.dump(config.to_dict(), file, indent=4)
|
568
|
+
return config
|
569
|
+
|
570
|
+
|
571
|
+
config_instance: Config = None
|
572
|
+
|
573
|
+
|
574
|
+
def get_config():
|
575
|
+
global config_instance
|
576
|
+
if config_instance is None:
|
577
|
+
config_instance = load_config()
|
578
|
+
config = config_instance.get_config()
|
579
|
+
|
580
|
+
if config.features.backfill_audio and config.features.full_auto:
|
581
|
+
logger.warning("Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
|
582
|
+
config.features.backfill_audio = False
|
583
|
+
|
584
|
+
# print(config_instance.get_config())
|
585
|
+
return config_instance.get_config()
|
586
|
+
|
587
|
+
|
588
|
+
def reload_config():
|
589
|
+
global config_instance
|
590
|
+
config_instance = load_config()
|
591
|
+
config = config_instance.get_config()
|
592
|
+
|
593
|
+
if config.features.backfill_audio and config.features.full_auto:
|
594
|
+
logger.warning("Backfill is enabled, but full auto is also enabled. Disabling backfill...")
|
595
|
+
config.features.backfill_audio = False
|
596
|
+
|
597
|
+
def get_master_config():
|
598
|
+
return config_instance
|
599
|
+
|
600
|
+
def save_full_config(config):
|
601
|
+
with open(get_config_path(), 'w') as file:
|
602
|
+
json.dump(config.to_dict(), file, indent=4)
|
603
|
+
|
604
|
+
def save_current_config(config):
|
605
|
+
global config_instance
|
606
|
+
config_instance.set_config_for_profile(config_instance.current_profile, config)
|
607
|
+
save_full_config(config_instance)
|
608
|
+
|
609
|
+
def switch_profile_and_save(profile_name):
|
610
|
+
global config_instance
|
611
|
+
config_instance.current_profile = profile_name
|
612
|
+
save_full_config(config_instance)
|
613
|
+
return config_instance.get_config()
|
614
|
+
|
615
|
+
|
616
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
617
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
618
|
+
|
619
|
+
logger = logging.getLogger("GameSentenceMiner")
|
620
|
+
logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
|
621
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
622
|
+
|
623
|
+
# Create console handler with level INFO
|
624
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
625
|
+
console_handler.setLevel(logging.INFO)
|
626
|
+
|
627
|
+
console_handler.setFormatter(formatter)
|
628
|
+
|
629
|
+
logger.addHandler(console_handler)
|
630
|
+
|
631
|
+
# Create rotating file handler with level DEBUG
|
632
|
+
if 'gsm' in sys.argv[0].lower() or 'gamesentenceminer' in sys.argv[0].lower():
|
633
|
+
file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
|
634
|
+
file_handler.setLevel(logging.DEBUG)
|
635
|
+
file_handler.setFormatter(formatter)
|
636
|
+
logger.addHandler(file_handler)
|
637
|
+
|
638
|
+
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
639
|
+
|
640
|
+
class GsmAppState:
|
641
|
+
def __init__(self):
|
642
|
+
self.line_for_audio = None
|
643
|
+
self.line_for_screenshot = None
|
644
|
+
self.previous_line_for_audio = None
|
645
|
+
self.previous_line_for_screenshot = None
|
646
|
+
self.previous_audio = None
|
647
|
+
self.previous_screenshot = None
|
648
|
+
self.previous_replay = None
|
649
|
+
self.lock = threading.Lock()
|
650
|
+
self.last_mined_line = None
|
651
|
+
self.keep_running = True
|
652
|
+
|
653
|
+
gsm_state = GsmAppState()
|