GameSentenceMiner 2.9.3__py3-none-any.whl → 2.9.5__py3-none-any.whl

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