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 CHANGED
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import subprocess
3
3
  import sys
4
- import threading
5
4
 
6
5
  from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
7
6
  from GameSentenceMiner.util.communication.send import send_restart_signal
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 = make_unique_file_name(os.path.abspath(
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
- response = client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
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()