GameSentenceMiner 2.13.15__py3-none-any.whl → 2.14.0rc1__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/ai_prompting.py +77 -132
- GameSentenceMiner/anki.py +48 -6
- GameSentenceMiner/config_gui.py +196 -30
- GameSentenceMiner/gametext.py +8 -19
- GameSentenceMiner/gsm.py +5 -4
- GameSentenceMiner/locales/en_us.json +21 -11
- GameSentenceMiner/locales/ja_jp.json +21 -11
- GameSentenceMiner/locales/zh_cn.json +9 -11
- GameSentenceMiner/owocr/owocr/ocr.py +20 -23
- GameSentenceMiner/tools/__init__.py +0 -0
- GameSentenceMiner/util/configuration.py +241 -105
- GameSentenceMiner/util/db.py +408 -0
- GameSentenceMiner/util/ffmpeg.py +2 -10
- GameSentenceMiner/util/get_overlay_coords.py +324 -0
- GameSentenceMiner/util/model.py +8 -2
- GameSentenceMiner/util/text_log.py +1 -1
- GameSentenceMiner/web/texthooking_page.py +1 -1
- GameSentenceMiner/wip/__init___.py +0 -0
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/METADATA +5 -1
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/RECORD +27 -25
- GameSentenceMiner/util/package.py +0 -37
- GameSentenceMiner/wip/get_overlay_coords.py +0 -535
- /GameSentenceMiner/{util → tools}/audio_offset_selector.py +0 -0
- /GameSentenceMiner/{util → tools}/ss_selector.py +0 -0
- /GameSentenceMiner/{util → tools}/window_transparency.py +0 -0
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/top_level.txt +0 -0
@@ -15,6 +15,10 @@ from enum import Enum
|
|
15
15
|
import toml
|
16
16
|
from dataclasses_json import dataclass_json
|
17
17
|
|
18
|
+
from importlib import metadata
|
19
|
+
|
20
|
+
import requests
|
21
|
+
|
18
22
|
|
19
23
|
OFF = 'OFF'
|
20
24
|
# VOSK = 'VOSK'
|
@@ -34,7 +38,7 @@ WHISPER_TURBO = 'turbo'
|
|
34
38
|
|
35
39
|
AI_GEMINI = 'Gemini'
|
36
40
|
AI_GROQ = 'Groq'
|
37
|
-
|
41
|
+
AI_OPENAI = 'OpenAI'
|
38
42
|
|
39
43
|
INFO = 'INFO'
|
40
44
|
DEBUG = 'DEBUG'
|
@@ -51,6 +55,7 @@ supported_formats = {
|
|
51
55
|
'm4a': 'aac',
|
52
56
|
}
|
53
57
|
|
58
|
+
|
54
59
|
def is_linux():
|
55
60
|
return platform == 'linux'
|
56
61
|
|
@@ -58,6 +63,7 @@ def is_linux():
|
|
58
63
|
def is_windows():
|
59
64
|
return platform == 'win32'
|
60
65
|
|
66
|
+
|
61
67
|
class Locale(Enum):
|
62
68
|
English = 'en_us'
|
63
69
|
日本語 = 'ja_jp'
|
@@ -86,7 +92,7 @@ class Locale(Enum):
|
|
86
92
|
return cls.from_any(item)
|
87
93
|
except KeyError:
|
88
94
|
raise
|
89
|
-
|
95
|
+
|
90
96
|
|
91
97
|
# Patch Enum's __getitem__ for this class
|
92
98
|
Locale.__getitem__ = classmethod(Locale.__getitem__)
|
@@ -111,13 +117,12 @@ class Language(Enum):
|
|
111
117
|
FINNISH = "fi"
|
112
118
|
DANISH = "da"
|
113
119
|
NORWEGIAN = "no"
|
114
|
-
|
115
|
-
|
116
|
-
|
120
|
+
|
117
121
|
|
118
122
|
AVAILABLE_LANGUAGES = [lang.value for lang in Language]
|
119
123
|
AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
|
120
124
|
|
125
|
+
|
121
126
|
class CommonLanguages(str, Enum):
|
122
127
|
"""
|
123
128
|
An Enum of the world's most common languages, based on total speaker count.
|
@@ -281,9 +286,9 @@ class CommonLanguages(str, Enum):
|
|
281
286
|
YORUBA = 'yo'
|
282
287
|
YUE_CHINESE = 'yue'
|
283
288
|
ZULU = 'zu'
|
284
|
-
|
285
289
|
|
286
290
|
# Helper methods
|
291
|
+
|
287
292
|
@classmethod
|
288
293
|
def get_all_codes(cls) -> list[str]:
|
289
294
|
"""Returns a list of all language codes (e.g., ['en', 'zh', 'hi'])."""
|
@@ -293,7 +298,7 @@ class CommonLanguages(str, Enum):
|
|
293
298
|
def get_all_names(cls) -> list[str]:
|
294
299
|
"""Returns a list of all language names (e.g., ['ENGLISH', 'MANDARIN_CHINESE'])."""
|
295
300
|
return [lang.name for lang in cls]
|
296
|
-
|
301
|
+
|
297
302
|
@classmethod
|
298
303
|
def get_all_names_pretty(cls) -> list[str]:
|
299
304
|
"""Returns a list of all language names formatted for display (e.g., ['English', 'Mandarin Chinese'])."""
|
@@ -308,7 +313,7 @@ class CommonLanguages(str, Enum):
|
|
308
313
|
Example: [('en', 'English'), ('zh', 'Mandarin Chinese')]
|
309
314
|
"""
|
310
315
|
return [(lang.value, lang.name.replace('_', ' ').title()) for lang in cls]
|
311
|
-
|
316
|
+
|
312
317
|
# Method to lookup language by it's name
|
313
318
|
@classmethod
|
314
319
|
def from_name(cls, name: str) -> 'CommonLanguages':
|
@@ -320,7 +325,7 @@ class CommonLanguages(str, Enum):
|
|
320
325
|
return cls[name.upper()]
|
321
326
|
except KeyError:
|
322
327
|
raise ValueError(f"Language '{name}' not found in CommonLanguages")
|
323
|
-
|
328
|
+
|
324
329
|
# Method to lookup language by its code
|
325
330
|
@classmethod
|
326
331
|
def from_code(cls, code: str) -> 'CommonLanguages':
|
@@ -331,8 +336,9 @@ class CommonLanguages(str, Enum):
|
|
331
336
|
for lang in cls:
|
332
337
|
if lang.value == code:
|
333
338
|
return lang
|
334
|
-
raise ValueError(
|
335
|
-
|
339
|
+
raise ValueError(
|
340
|
+
f"Language code '{code}' not found in CommonLanguages")
|
341
|
+
|
336
342
|
@classmethod
|
337
343
|
def name_from_code(cls, code: str) -> str:
|
338
344
|
"""
|
@@ -341,6 +347,44 @@ class CommonLanguages(str, Enum):
|
|
341
347
|
"""
|
342
348
|
return cls.from_code(code).name
|
343
349
|
|
350
|
+
|
351
|
+
PACKAGE_NAME = "GameSentenceMiner"
|
352
|
+
|
353
|
+
|
354
|
+
def get_current_version():
|
355
|
+
try:
|
356
|
+
version = metadata.version(PACKAGE_NAME)
|
357
|
+
return version
|
358
|
+
except metadata.PackageNotFoundError:
|
359
|
+
return None
|
360
|
+
|
361
|
+
|
362
|
+
def get_latest_version():
|
363
|
+
try:
|
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
|
+
|
344
388
|
@dataclass_json
|
345
389
|
@dataclass
|
346
390
|
class General:
|
@@ -380,6 +424,7 @@ class Paths:
|
|
380
424
|
if self.output_folder:
|
381
425
|
self.output_folder = os.path.normpath(self.output_folder)
|
382
426
|
|
427
|
+
|
383
428
|
@dataclass_json
|
384
429
|
@dataclass
|
385
430
|
class Anki:
|
@@ -391,7 +436,8 @@ class Anki:
|
|
391
436
|
word_field: str = 'Expression'
|
392
437
|
previous_sentence_field: str = ''
|
393
438
|
previous_image_field: str = ''
|
394
|
-
|
439
|
+
# Initialize to None and set it in __post_init__
|
440
|
+
custom_tags: List[str] = None
|
395
441
|
tags_to_check: List[str] = None
|
396
442
|
add_game_tag: bool = True
|
397
443
|
polling_rate: int = 200
|
@@ -444,7 +490,6 @@ class Screenshot:
|
|
444
490
|
self.screenshot_timing_setting = 'end'
|
445
491
|
|
446
492
|
|
447
|
-
|
448
493
|
@dataclass_json
|
449
494
|
@dataclass
|
450
495
|
class Audio:
|
@@ -461,14 +506,15 @@ class Audio:
|
|
461
506
|
custom_encode_settings: str = ''
|
462
507
|
|
463
508
|
def __post_init__(self):
|
464
|
-
self.ffmpeg_reencode_options_to_use = self.ffmpeg_reencode_options.replace(
|
509
|
+
self.ffmpeg_reencode_options_to_use = self.ffmpeg_reencode_options.replace(
|
510
|
+
"{format}", self.extension).replace("{encoder}", supported_formats.get(self.extension, ''))
|
465
511
|
if self.anki_media_collection:
|
466
|
-
self.anki_media_collection = os.path.normpath(
|
512
|
+
self.anki_media_collection = os.path.normpath(
|
513
|
+
self.anki_media_collection)
|
467
514
|
if self.external_tool:
|
468
515
|
self.external_tool = os.path.normpath(self.external_tool)
|
469
516
|
|
470
517
|
|
471
|
-
|
472
518
|
@dataclass_json
|
473
519
|
@dataclass
|
474
520
|
class OBS:
|
@@ -542,11 +588,13 @@ class Ai:
|
|
542
588
|
anki_field: str = ''
|
543
589
|
provider: str = AI_GEMINI
|
544
590
|
gemini_model: str = 'gemini-2.5-flash-lite'
|
545
|
-
local_model: str = OFF
|
546
591
|
groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
547
|
-
api_key: str = '' # Deprecated
|
548
592
|
gemini_api_key: str = ''
|
593
|
+
api_key: str = '' # Legacy support, will be moved to gemini_api_key if provider is gemini
|
549
594
|
groq_api_key: str = ''
|
595
|
+
open_ai_url: str = ''
|
596
|
+
open_ai_model: str = ''
|
597
|
+
open_ai_api_key: str = ''
|
550
598
|
use_canned_translation_prompt: bool = True
|
551
599
|
use_canned_context_prompt: bool = False
|
552
600
|
custom_prompt: str = ''
|
@@ -559,24 +607,31 @@ class Ai:
|
|
559
607
|
self.provider = AI_GEMINI
|
560
608
|
if self.provider == 'groq':
|
561
609
|
self.provider = AI_GROQ
|
562
|
-
|
610
|
+
if self.gemini_model in ['RECOMMENDED', 'OTHER']:
|
611
|
+
self.gemini_model = 'gemini-2.5-flash-lite'
|
612
|
+
if self.groq_model in ['RECOMMENDED', 'OTHER']:
|
613
|
+
self.groq_model = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
614
|
+
|
563
615
|
# Change Legacy Model Name
|
564
616
|
if self.gemini_model == 'gemini-2.5-flash-lite-preview-06-17':
|
565
617
|
self.gemini_model = 'gemini-2.5-flash-lite'
|
566
|
-
|
567
|
-
|
568
|
-
# Experimental Features section, will change often
|
618
|
+
|
619
|
+
|
569
620
|
@dataclass_json
|
570
621
|
@dataclass
|
571
|
-
class
|
572
|
-
|
573
|
-
overlay_websocket_send: bool = False
|
622
|
+
class Overlay:
|
623
|
+
websocket_port: int = 55499
|
574
624
|
monitor_to_capture: int = 0
|
575
|
-
|
625
|
+
|
576
626
|
def __post_init__(self):
|
577
627
|
if self.monitor_to_capture == -1:
|
578
628
|
self.monitor_to_capture = 0 # Default to the first monitor if not set
|
579
|
-
|
629
|
+
|
630
|
+
|
631
|
+
@dataclass_json
|
632
|
+
@dataclass
|
633
|
+
class WIP:
|
634
|
+
pass
|
580
635
|
|
581
636
|
|
582
637
|
@dataclass_json
|
@@ -595,68 +650,96 @@ class ProfileConfig:
|
|
595
650
|
vad: VAD = field(default_factory=VAD)
|
596
651
|
advanced: Advanced = field(default_factory=Advanced)
|
597
652
|
ai: Ai = field(default_factory=Ai)
|
653
|
+
overlay: Overlay = field(default_factory=Overlay)
|
598
654
|
wip: WIP = field(default_factory=WIP)
|
599
|
-
|
600
|
-
|
655
|
+
|
601
656
|
def get_field_value(self, section: str, field_name: str):
|
602
657
|
section_obj = getattr(self, section, None)
|
603
658
|
if section_obj and hasattr(section_obj, field_name):
|
604
659
|
return getattr(section_obj, field_name)
|
605
660
|
else:
|
606
|
-
raise ValueError(
|
661
|
+
raise ValueError(
|
662
|
+
f"Field '{field_name}' not found in section '{section}' of ProfileConfig.")
|
607
663
|
|
608
664
|
# This is just for legacy support
|
609
665
|
def load_from_toml(self, file_path: str):
|
610
666
|
with open(file_path, 'r') as f:
|
611
667
|
config_data = toml.load(f)
|
612
668
|
|
613
|
-
self.paths.folder_to_watch = expanduser(config_data['paths'].get(
|
669
|
+
self.paths.folder_to_watch = expanduser(config_data['paths'].get(
|
670
|
+
'folder_to_watch', self.paths.folder_to_watch))
|
614
671
|
|
615
672
|
self.anki.url = config_data['anki'].get('url', self.anki.url)
|
616
|
-
self.anki.sentence_field = config_data['anki'].get(
|
617
|
-
|
618
|
-
self.anki.
|
619
|
-
|
620
|
-
self.anki.
|
621
|
-
|
622
|
-
self.anki.
|
623
|
-
|
673
|
+
self.anki.sentence_field = config_data['anki'].get(
|
674
|
+
'sentence_field', self.anki.sentence_field)
|
675
|
+
self.anki.sentence_audio_field = config_data['anki'].get(
|
676
|
+
'sentence_audio_field', self.anki.sentence_audio_field)
|
677
|
+
self.anki.word_field = config_data['anki'].get(
|
678
|
+
'word_field', self.anki.word_field)
|
679
|
+
self.anki.picture_field = config_data['anki'].get(
|
680
|
+
'picture_field', self.anki.picture_field)
|
681
|
+
self.anki.custom_tags = config_data['anki'].get(
|
682
|
+
'custom_tags', self.anki.custom_tags)
|
683
|
+
self.anki.add_game_tag = config_data['anki'].get(
|
684
|
+
'add_game_tag', self.anki.add_game_tag)
|
685
|
+
self.anki.polling_rate = config_data['anki'].get(
|
686
|
+
'polling_rate', self.anki.polling_rate)
|
687
|
+
self.anki.overwrite_audio = config_data['anki_overwrites'].get(
|
688
|
+
'overwrite_audio', self.anki.overwrite_audio)
|
624
689
|
self.anki.overwrite_picture = config_data['anki_overwrites'].get('overwrite_picture',
|
625
690
|
self.anki.overwrite_picture)
|
626
691
|
|
627
|
-
self.features.full_auto = config_data['features'].get(
|
628
|
-
|
629
|
-
self.features.
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
self.
|
634
|
-
|
635
|
-
|
692
|
+
self.features.full_auto = config_data['features'].get(
|
693
|
+
'do_vosk_postprocessing', self.features.full_auto)
|
694
|
+
self.features.notify_on_update = config_data['features'].get(
|
695
|
+
'notify_on_update', self.features.notify_on_update)
|
696
|
+
self.features.open_anki_edit = config_data['features'].get(
|
697
|
+
'open_anki_edit', self.features.open_anki_edit)
|
698
|
+
self.features.backfill_audio = config_data['features'].get(
|
699
|
+
'backfill_audio', self.features.backfill_audio)
|
700
|
+
|
701
|
+
self.screenshot.width = config_data['screenshot'].get(
|
702
|
+
'width', self.screenshot.width)
|
703
|
+
self.screenshot.height = config_data['screenshot'].get(
|
704
|
+
'height', self.screenshot.height)
|
705
|
+
self.screenshot.quality = config_data['screenshot'].get(
|
706
|
+
'quality', self.screenshot.quality)
|
707
|
+
self.screenshot.extension = config_data['screenshot'].get(
|
708
|
+
'extension', self.screenshot.extension)
|
636
709
|
self.screenshot.custom_ffmpeg_settings = config_data['screenshot'].get('custom_ffmpeg_settings',
|
637
710
|
self.screenshot.custom_ffmpeg_settings)
|
638
711
|
|
639
|
-
self.audio.extension = config_data['audio'].get(
|
640
|
-
|
641
|
-
self.audio.
|
712
|
+
self.audio.extension = config_data['audio'].get(
|
713
|
+
'extension', self.audio.extension)
|
714
|
+
self.audio.beginning_offset = config_data['audio'].get(
|
715
|
+
'beginning_offset', self.audio.beginning_offset)
|
716
|
+
self.audio.end_offset = config_data['audio'].get(
|
717
|
+
'end_offset', self.audio.end_offset)
|
642
718
|
self.audio.ffmpeg_reencode_options = config_data['audio'].get('ffmpeg_reencode_options',
|
643
719
|
self.audio.ffmpeg_reencode_options)
|
644
720
|
|
645
|
-
self.vad.whisper_model = config_data['vosk'].get(
|
721
|
+
self.vad.whisper_model = config_data['vosk'].get(
|
722
|
+
'whisper_model', self.vad.whisper_model)
|
646
723
|
self.vad.vosk_url = config_data['vosk'].get('url', self.vad.vosk_url)
|
647
724
|
self.vad.do_vad_postprocessing = config_data['features'].get('do_vosk_postprocessing',
|
648
725
|
self.vad.do_vad_postprocessing)
|
649
|
-
self.vad.trim_beginning = config_data['audio'].get(
|
726
|
+
self.vad.trim_beginning = config_data['audio'].get(
|
727
|
+
'vosk_trim_beginning', self.vad.trim_beginning)
|
650
728
|
|
651
729
|
self.obs.host = config_data['obs'].get('host', self.obs.host)
|
652
730
|
self.obs.port = config_data['obs'].get('port', self.obs.port)
|
653
|
-
self.obs.password = config_data['obs'].get(
|
731
|
+
self.obs.password = config_data['obs'].get(
|
732
|
+
'password', self.obs.password)
|
654
733
|
|
655
|
-
self.general.use_websocket = config_data['websocket'].get(
|
656
|
-
|
734
|
+
self.general.use_websocket = config_data['websocket'].get(
|
735
|
+
'enabled', self.general.use_websocket)
|
736
|
+
self.general.websocket_uri = config_data['websocket'].get(
|
737
|
+
'uri', self.general.websocket_uri)
|
657
738
|
|
658
|
-
self.hotkeys.reset_line = config_data['hotkeys'].get(
|
659
|
-
|
739
|
+
self.hotkeys.reset_line = config_data['hotkeys'].get(
|
740
|
+
'reset_line', self.hotkeys.reset_line)
|
741
|
+
self.hotkeys.take_screenshot = config_data['hotkeys'].get(
|
742
|
+
'take_screenshot', self.hotkeys.take_screenshot)
|
660
743
|
|
661
744
|
with open(get_config_path(), 'w') as f:
|
662
745
|
f.write(self.to_json(indent=4))
|
@@ -690,14 +773,16 @@ class Config:
|
|
690
773
|
|
691
774
|
@classmethod
|
692
775
|
def new(cls):
|
693
|
-
instance = cls(
|
776
|
+
instance = cls(
|
777
|
+
configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
|
694
778
|
return instance
|
695
|
-
|
779
|
+
|
696
780
|
def get_locale(self) -> Locale:
|
697
781
|
try:
|
698
782
|
return Locale.from_any(self.locale)
|
699
783
|
except KeyError:
|
700
|
-
logger.warning(
|
784
|
+
logger.warning(
|
785
|
+
f"Locale '{self.locale}' not found. Defaulting to English.")
|
701
786
|
return Locale.English
|
702
787
|
|
703
788
|
@classmethod
|
@@ -717,7 +802,8 @@ class Config:
|
|
717
802
|
|
718
803
|
def get_config(self) -> ProfileConfig:
|
719
804
|
if self.current_profile not in self.configs:
|
720
|
-
logger.warning(
|
805
|
+
logger.warning(
|
806
|
+
f"Profile '{self.current_profile}' not found. Switching to default profile.")
|
721
807
|
self.current_profile = DEFAULT_CONFIG
|
722
808
|
return self.configs[self.current_profile]
|
723
809
|
|
@@ -741,49 +827,74 @@ class Config:
|
|
741
827
|
if dataclasses.is_dataclass(getattr(current_config, section, None)):
|
742
828
|
for field_name in getattr(current_config, section, None).to_dict():
|
743
829
|
config_section = getattr(current_config, section, None)
|
744
|
-
previous_config_section = getattr(
|
830
|
+
previous_config_section = getattr(
|
831
|
+
previous_config, section, None)
|
745
832
|
current_value = getattr(config_section, field_name, None)
|
746
|
-
previous_value = getattr(
|
833
|
+
previous_value = getattr(
|
834
|
+
previous_config_section, field_name, None)
|
747
835
|
if str(current_value).strip() != str(previous_value).strip():
|
748
|
-
logger.info(
|
836
|
+
logger.info(
|
837
|
+
f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
|
749
838
|
for profile in self.configs.values():
|
750
839
|
if profile != current_config:
|
751
|
-
profile_section = getattr(
|
840
|
+
profile_section = getattr(
|
841
|
+
profile, section, None)
|
752
842
|
if profile_section:
|
753
|
-
setattr(profile_section,
|
754
|
-
|
843
|
+
setattr(profile_section,
|
844
|
+
field_name, current_value)
|
845
|
+
logger.info(
|
846
|
+
f"Updated '{field_name}' in profile '{profile.name}'")
|
755
847
|
|
756
848
|
return self
|
757
849
|
|
758
850
|
def sync_shared_fields(self):
|
759
851
|
config = self.get_config()
|
760
852
|
for profile in self.configs.values():
|
761
|
-
self.sync_shared_field(
|
762
|
-
|
763
|
-
self.sync_shared_field(
|
764
|
-
|
853
|
+
self.sync_shared_field(
|
854
|
+
config.hotkeys, profile.hotkeys, "reset_line")
|
855
|
+
self.sync_shared_field(
|
856
|
+
config.hotkeys, profile.hotkeys, "take_screenshot")
|
857
|
+
self.sync_shared_field(
|
858
|
+
config.hotkeys, profile.hotkeys, "open_utility")
|
859
|
+
self.sync_shared_field(
|
860
|
+
config.hotkeys, profile.hotkeys, "play_latest_audio")
|
765
861
|
self.sync_shared_field(config.anki, profile.anki, "url")
|
766
862
|
self.sync_shared_field(config.anki, profile.anki, "sentence_field")
|
767
|
-
self.sync_shared_field(
|
863
|
+
self.sync_shared_field(
|
864
|
+
config.anki, profile.anki, "sentence_audio_field")
|
768
865
|
self.sync_shared_field(config.anki, profile.anki, "picture_field")
|
769
866
|
self.sync_shared_field(config.anki, profile.anki, "word_field")
|
770
|
-
self.sync_shared_field(
|
771
|
-
|
867
|
+
self.sync_shared_field(
|
868
|
+
config.anki, profile.anki, "previous_sentence_field")
|
869
|
+
self.sync_shared_field(
|
870
|
+
config.anki, profile.anki, "previous_image_field")
|
772
871
|
self.sync_shared_field(config.anki, profile.anki, "tags_to_check")
|
773
872
|
self.sync_shared_field(config.anki, profile.anki, "add_game_tag")
|
774
873
|
self.sync_shared_field(config.anki, profile.anki, "polling_rate")
|
775
|
-
self.sync_shared_field(
|
776
|
-
|
777
|
-
self.sync_shared_field(
|
778
|
-
|
779
|
-
self.sync_shared_field(
|
780
|
-
|
781
|
-
self.sync_shared_field(
|
782
|
-
|
783
|
-
self.sync_shared_field(
|
784
|
-
|
785
|
-
self.sync_shared_field(
|
786
|
-
|
874
|
+
self.sync_shared_field(
|
875
|
+
config.anki, profile.anki, "overwrite_audio")
|
876
|
+
self.sync_shared_field(
|
877
|
+
config.anki, profile.anki, "overwrite_picture")
|
878
|
+
self.sync_shared_field(
|
879
|
+
config.anki, profile.anki, "multi_overwrites_sentence")
|
880
|
+
self.sync_shared_field(
|
881
|
+
config.general, profile.general, "open_config_on_startup")
|
882
|
+
self.sync_shared_field(
|
883
|
+
config.general, profile.general, "open_multimine_on_startup")
|
884
|
+
self.sync_shared_field(
|
885
|
+
config.general, profile.general, "websocket_uri")
|
886
|
+
self.sync_shared_field(
|
887
|
+
config.general, profile.general, "texthooker_port")
|
888
|
+
self.sync_shared_field(
|
889
|
+
config.audio, profile.audio, "external_tool")
|
890
|
+
self.sync_shared_field(
|
891
|
+
config.audio, profile.audio, "anki_media_collection")
|
892
|
+
self.sync_shared_field(
|
893
|
+
config.audio, profile.audio, "external_tool_enabled")
|
894
|
+
self.sync_shared_field(
|
895
|
+
config.audio, profile.audio, "custom_encode_settings")
|
896
|
+
self.sync_shared_field(
|
897
|
+
config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
|
787
898
|
self.sync_shared_field(config, profile, "advanced")
|
788
899
|
self.sync_shared_field(config, profile, "paths")
|
789
900
|
self.sync_shared_field(config, profile, "obs")
|
@@ -796,7 +907,6 @@ class Config:
|
|
796
907
|
|
797
908
|
return self
|
798
909
|
|
799
|
-
|
800
910
|
def sync_shared_field(self, config, config2, field_name):
|
801
911
|
try:
|
802
912
|
config_value = getattr(config, field_name, None)
|
@@ -804,15 +914,18 @@ class Config:
|
|
804
914
|
|
805
915
|
if config_value != config2_value: # Check if values are different.
|
806
916
|
if config_value is not None:
|
807
|
-
logging.info(
|
917
|
+
logging.info(
|
918
|
+
f"Syncing shared field '{field_name}' to other profile.")
|
808
919
|
setattr(config2, field_name, config_value)
|
809
920
|
elif config2_value is not None:
|
810
|
-
logging.info(
|
921
|
+
logging.info(
|
922
|
+
f"Syncing shared field '{field_name}' to current profile.")
|
811
923
|
setattr(config, field_name, config2_value)
|
812
924
|
except AttributeError as e:
|
813
925
|
logging.error(f"AttributeError during sync of '{field_name}': {e}")
|
814
926
|
except Exception as e:
|
815
|
-
logging.error(
|
927
|
+
logging.error(
|
928
|
+
f"An unexpected error occurred during sync of '{field_name}': {e}")
|
816
929
|
|
817
930
|
|
818
931
|
def get_default_anki_path():
|
@@ -823,16 +936,19 @@ def get_default_anki_path():
|
|
823
936
|
config_dir = os.path.join(base_dir, 'Anki2')
|
824
937
|
return config_dir
|
825
938
|
|
939
|
+
|
826
940
|
def get_default_anki_media_collection_path():
|
827
941
|
return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
|
828
942
|
|
943
|
+
|
829
944
|
def get_app_directory():
|
830
945
|
if platform == 'win32': # Windows
|
831
946
|
appdata_dir = os.getenv('APPDATA')
|
832
947
|
else: # macOS and Linux
|
833
948
|
appdata_dir = os.path.expanduser('~/.config')
|
834
949
|
config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
|
835
|
-
|
950
|
+
# Create the directory if it doesn't exist
|
951
|
+
os.makedirs(config_dir, exist_ok=True)
|
836
952
|
return config_dir
|
837
953
|
|
838
954
|
|
@@ -841,8 +957,10 @@ def get_log_path():
|
|
841
957
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
842
958
|
return path
|
843
959
|
|
960
|
+
|
844
961
|
temp_directory = ''
|
845
962
|
|
963
|
+
|
846
964
|
def get_temporary_directory(delete=False):
|
847
965
|
global temp_directory
|
848
966
|
if not temp_directory:
|
@@ -860,6 +978,7 @@ def get_temporary_directory(delete=False):
|
|
860
978
|
logger.error(f"Failed to delete {file_path}. Reason: {e}")
|
861
979
|
return temp_directory
|
862
980
|
|
981
|
+
|
863
982
|
def get_config_path():
|
864
983
|
return os.path.join(get_app_directory(), 'config.json')
|
865
984
|
|
@@ -869,7 +988,7 @@ def load_config():
|
|
869
988
|
|
870
989
|
if os.path.exists('config.json') and not os.path.exists(config_path):
|
871
990
|
shutil.copy('config.json', config_path)
|
872
|
-
|
991
|
+
|
873
992
|
if os.path.exists(config_path):
|
874
993
|
try:
|
875
994
|
with open(config_path, 'r') as file:
|
@@ -882,19 +1001,22 @@ def load_config():
|
|
882
1001
|
config_file = json.load(file)
|
883
1002
|
|
884
1003
|
config = ProfileConfig.from_dict(config_file)
|
885
|
-
new_config = Config(
|
1004
|
+
new_config = Config(
|
1005
|
+
configs={DEFAULT_CONFIG: config}, current_profile=DEFAULT_CONFIG)
|
886
1006
|
|
887
1007
|
config.save()
|
888
1008
|
return new_config
|
889
1009
|
except json.JSONDecodeError as e:
|
890
|
-
logger.error(
|
1010
|
+
logger.error(
|
1011
|
+
f"Error parsing config.json, saving backup and returning new config: {e}")
|
891
1012
|
shutil.copy(config_path, config_path + '.bak')
|
892
1013
|
config = Config.new()
|
893
1014
|
config.save()
|
894
1015
|
return config
|
895
1016
|
elif os.path.exists('config.toml'):
|
896
1017
|
config = ProfileConfig().load_from_toml('config.toml')
|
897
|
-
new_config = Config({DEFAULT_CONFIG: config},
|
1018
|
+
new_config = Config({DEFAULT_CONFIG: config},
|
1019
|
+
current_profile=DEFAULT_CONFIG)
|
898
1020
|
return new_config
|
899
1021
|
else:
|
900
1022
|
config = Config.new()
|
@@ -912,7 +1034,8 @@ def get_config():
|
|
912
1034
|
config = config_instance.get_config()
|
913
1035
|
|
914
1036
|
if config.features.backfill_audio and config.features.full_auto:
|
915
|
-
logger.warning(
|
1037
|
+
logger.warning(
|
1038
|
+
"Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
|
916
1039
|
config.features.backfill_audio = False
|
917
1040
|
|
918
1041
|
# print(config_instance.get_config())
|
@@ -925,21 +1048,27 @@ def reload_config():
|
|
925
1048
|
config = config_instance.get_config()
|
926
1049
|
|
927
1050
|
if config.features.backfill_audio and config.features.full_auto:
|
928
|
-
logger.warning(
|
1051
|
+
logger.warning(
|
1052
|
+
"Backfill is enabled, but full auto is also enabled. Disabling backfill...")
|
929
1053
|
config.features.backfill_audio = False
|
930
1054
|
|
1055
|
+
|
931
1056
|
def get_master_config():
|
932
1057
|
return config_instance
|
933
1058
|
|
1059
|
+
|
934
1060
|
def save_full_config(config):
|
935
1061
|
with open(get_config_path(), 'w') as file:
|
936
1062
|
json.dump(config.to_dict(), file, indent=4)
|
937
1063
|
|
1064
|
+
|
938
1065
|
def save_current_config(config):
|
939
1066
|
global config_instance
|
940
|
-
config_instance.set_config_for_profile(
|
1067
|
+
config_instance.set_config_for_profile(
|
1068
|
+
config_instance.current_profile, config)
|
941
1069
|
save_full_config(config_instance)
|
942
1070
|
|
1071
|
+
|
943
1072
|
def switch_profile_and_save(profile_name):
|
944
1073
|
global config_instance
|
945
1074
|
config_instance.current_profile = profile_name
|
@@ -951,8 +1080,10 @@ sys.stdout.reconfigure(encoding='utf-8')
|
|
951
1080
|
sys.stderr.reconfigure(encoding='utf-8')
|
952
1081
|
|
953
1082
|
logger = logging.getLogger("GameSentenceMiner")
|
954
|
-
|
955
|
-
|
1083
|
+
# Set the base level to DEBUG so that all messages are captured
|
1084
|
+
logger.setLevel(logging.DEBUG)
|
1085
|
+
formatter = logging.Formatter(
|
1086
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
956
1087
|
|
957
1088
|
# Create console handler with level INFO
|
958
1089
|
console_handler = logging.StreamHandler(sys.stdout)
|
@@ -965,12 +1096,14 @@ logger.addHandler(console_handler)
|
|
965
1096
|
file_path = get_log_path()
|
966
1097
|
try:
|
967
1098
|
if os.path.exists(file_path) and os.path.getsize(file_path) > 1 * 1024 * 1024 and os.access(file_path, os.W_OK):
|
968
|
-
old_log_path = os.path.join(os.path.dirname(
|
1099
|
+
old_log_path = os.path.join(os.path.dirname(
|
1100
|
+
file_path), "gamesentenceminer_old.log")
|
969
1101
|
if os.path.exists(old_log_path):
|
970
1102
|
os.remove(old_log_path)
|
971
1103
|
shutil.move(file_path, old_log_path)
|
972
1104
|
except Exception as e:
|
973
|
-
logger.info(
|
1105
|
+
logger.info(
|
1106
|
+
"Couldn't rotate log, probably because the file is being written to by another process. NOT AN ERROR")
|
974
1107
|
|
975
1108
|
file_handler = logging.FileHandler(file_path, encoding='utf-8')
|
976
1109
|
file_handler.setLevel(logging.DEBUG)
|
@@ -979,6 +1112,7 @@ logger.addHandler(file_handler)
|
|
979
1112
|
|
980
1113
|
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
981
1114
|
|
1115
|
+
|
982
1116
|
class GsmAppState:
|
983
1117
|
def __init__(self):
|
984
1118
|
self.line_for_audio = None
|
@@ -996,6 +1130,7 @@ class GsmAppState:
|
|
996
1130
|
self.current_game = ''
|
997
1131
|
self.videos_to_remove = set()
|
998
1132
|
|
1133
|
+
|
999
1134
|
@dataclass_json
|
1000
1135
|
@dataclass
|
1001
1136
|
class AnkiUpdateResult:
|
@@ -1037,7 +1172,7 @@ def is_running_from_source():
|
|
1037
1172
|
# Check for .git directory at the project root
|
1038
1173
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
1039
1174
|
project_root = current_dir
|
1040
|
-
while project_root != os.path.dirname(project_root):
|
1175
|
+
while project_root != os.path.dirname(project_root): # Avoid infinite loop
|
1041
1176
|
if os.path.isdir(os.path.join(project_root, '.git')):
|
1042
1177
|
return True
|
1043
1178
|
if os.path.isfile(os.path.join(project_root, 'pyproject.toml')):
|
@@ -1045,6 +1180,7 @@ def is_running_from_source():
|
|
1045
1180
|
project_root = os.path.dirname(project_root)
|
1046
1181
|
return False
|
1047
1182
|
|
1183
|
+
|
1048
1184
|
gsm_status = GsmStatus()
|
1049
1185
|
anki_results = {}
|
1050
1186
|
gsm_state = GsmAppState()
|
@@ -1053,4 +1189,4 @@ is_dev = is_running_from_source()
|
|
1053
1189
|
is_beangate = os.path.exists("C:/Users/Beangate")
|
1054
1190
|
|
1055
1191
|
logger.debug(f"Running in development mode: {is_dev}")
|
1056
|
-
logger.debug(f"Running on Beangate's PC: {is_beangate}")
|
1192
|
+
logger.debug(f"Running on Beangate's PC: {is_beangate}")
|