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.
Files changed (29) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +77 -132
  2. GameSentenceMiner/anki.py +48 -6
  3. GameSentenceMiner/config_gui.py +196 -30
  4. GameSentenceMiner/gametext.py +8 -19
  5. GameSentenceMiner/gsm.py +5 -4
  6. GameSentenceMiner/locales/en_us.json +21 -11
  7. GameSentenceMiner/locales/ja_jp.json +21 -11
  8. GameSentenceMiner/locales/zh_cn.json +9 -11
  9. GameSentenceMiner/owocr/owocr/ocr.py +20 -23
  10. GameSentenceMiner/tools/__init__.py +0 -0
  11. GameSentenceMiner/util/configuration.py +241 -105
  12. GameSentenceMiner/util/db.py +408 -0
  13. GameSentenceMiner/util/ffmpeg.py +2 -10
  14. GameSentenceMiner/util/get_overlay_coords.py +324 -0
  15. GameSentenceMiner/util/model.py +8 -2
  16. GameSentenceMiner/util/text_log.py +1 -1
  17. GameSentenceMiner/web/texthooking_page.py +1 -1
  18. GameSentenceMiner/wip/__init___.py +0 -0
  19. {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/METADATA +5 -1
  20. {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/RECORD +27 -25
  21. GameSentenceMiner/util/package.py +0 -37
  22. GameSentenceMiner/wip/get_overlay_coords.py +0 -535
  23. /GameSentenceMiner/{util → tools}/audio_offset_selector.py +0 -0
  24. /GameSentenceMiner/{util → tools}/ss_selector.py +0 -0
  25. /GameSentenceMiner/{util → tools}/window_transparency.py +0 -0
  26. {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/WHEEL +0 -0
  27. {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/entry_points.txt +0 -0
  28. {gamesentenceminer-2.13.15.dist-info → gamesentenceminer-2.14.0rc1.dist-info}/licenses/LICENSE +0 -0
  29. {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
- AI_LOCAL = 'Local'
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(f"Language code '{code}' not found in CommonLanguages")
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
- custom_tags: List[str] = None # Initialize to None and set it in __post_init__
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("{format}", self.extension).replace("{encoder}", supported_formats.get(self.extension, ''))
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(self.anki_media_collection)
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 WIP:
572
- overlay_websocket_port: int = 55499
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(f"Field '{field_name}' not found in section '{section}' of ProfileConfig.")
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('folder_to_watch', self.paths.folder_to_watch))
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('sentence_field', self.anki.sentence_field)
617
- self.anki.sentence_audio_field = config_data['anki'].get('sentence_audio_field', self.anki.sentence_audio_field)
618
- self.anki.word_field = config_data['anki'].get('word_field', self.anki.word_field)
619
- self.anki.picture_field = config_data['anki'].get('picture_field', self.anki.picture_field)
620
- self.anki.custom_tags = config_data['anki'].get('custom_tags', self.anki.custom_tags)
621
- self.anki.add_game_tag = config_data['anki'].get('add_game_tag', self.anki.add_game_tag)
622
- self.anki.polling_rate = config_data['anki'].get('polling_rate', self.anki.polling_rate)
623
- self.anki.overwrite_audio = config_data['anki_overwrites'].get('overwrite_audio', self.anki.overwrite_audio)
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('do_vosk_postprocessing', self.features.full_auto)
628
- self.features.notify_on_update = config_data['features'].get('notify_on_update', self.features.notify_on_update)
629
- self.features.open_anki_edit = config_data['features'].get('open_anki_edit', self.features.open_anki_edit)
630
- self.features.backfill_audio = config_data['features'].get('backfill_audio', self.features.backfill_audio)
631
-
632
- self.screenshot.width = config_data['screenshot'].get('width', self.screenshot.width)
633
- self.screenshot.height = config_data['screenshot'].get('height', self.screenshot.height)
634
- self.screenshot.quality = config_data['screenshot'].get('quality', self.screenshot.quality)
635
- self.screenshot.extension = config_data['screenshot'].get('extension', self.screenshot.extension)
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('extension', self.audio.extension)
640
- self.audio.beginning_offset = config_data['audio'].get('beginning_offset', self.audio.beginning_offset)
641
- self.audio.end_offset = config_data['audio'].get('end_offset', self.audio.end_offset)
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('whisper_model', self.vad.whisper_model)
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('vosk_trim_beginning', self.vad.trim_beginning)
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('password', self.obs.password)
731
+ self.obs.password = config_data['obs'].get(
732
+ 'password', self.obs.password)
654
733
 
655
- self.general.use_websocket = config_data['websocket'].get('enabled', self.general.use_websocket)
656
- self.general.websocket_uri = config_data['websocket'].get('uri', self.general.websocket_uri)
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('reset_line', self.hotkeys.reset_line)
659
- self.hotkeys.take_screenshot = config_data['hotkeys'].get('take_screenshot', self.hotkeys.take_screenshot)
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(configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
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(f"Locale '{self.locale}' not found. Defaulting to English.")
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(f"Profile '{self.current_profile}' not found. Switching to default profile.")
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(previous_config, section, None)
830
+ previous_config_section = getattr(
831
+ previous_config, section, None)
745
832
  current_value = getattr(config_section, field_name, None)
746
- previous_value = getattr(previous_config_section, field_name, None)
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(f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
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(profile, section, None)
840
+ profile_section = getattr(
841
+ profile, section, None)
752
842
  if profile_section:
753
- setattr(profile_section, field_name, current_value)
754
- logger.info(f"Updated '{field_name}' in profile '{profile.name}'")
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(config.hotkeys, profile.hotkeys, "reset_line")
762
- self.sync_shared_field(config.hotkeys, profile.hotkeys, "take_screenshot")
763
- self.sync_shared_field(config.hotkeys, profile.hotkeys, "open_utility")
764
- self.sync_shared_field(config.hotkeys, profile.hotkeys, "play_latest_audio")
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(config.anki, profile.anki, "sentence_audio_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(config.anki, profile.anki, "previous_sentence_field")
771
- self.sync_shared_field(config.anki, profile.anki, "previous_image_field")
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(config.anki, profile.anki, "overwrite_audio")
776
- self.sync_shared_field(config.anki, profile.anki, "overwrite_picture")
777
- self.sync_shared_field(config.anki, profile.anki, "multi_overwrites_sentence")
778
- self.sync_shared_field(config.general, profile.general, "open_config_on_startup")
779
- self.sync_shared_field(config.general, profile.general, "open_multimine_on_startup")
780
- self.sync_shared_field(config.general, profile.general, "websocket_uri")
781
- self.sync_shared_field(config.general, profile.general, "texthooker_port")
782
- self.sync_shared_field(config.audio, profile.audio, "external_tool")
783
- self.sync_shared_field(config.audio, profile.audio, "anki_media_collection")
784
- self.sync_shared_field(config.audio, profile.audio, "external_tool_enabled")
785
- self.sync_shared_field(config.audio, profile.audio, "custom_encode_settings")
786
- self.sync_shared_field(config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
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(f"Syncing shared field '{field_name}' to other profile.")
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(f"Syncing shared field '{field_name}' to current profile.")
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(f"An unexpected error occurred during sync of '{field_name}': {e}")
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
- os.makedirs(config_dir, exist_ok=True) # Create the directory if it doesn't exist
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(configs = {DEFAULT_CONFIG : config}, current_profile=DEFAULT_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(f"Error parsing config.json, saving backup and returning new config: {e}")
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}, current_profile=DEFAULT_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("Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
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("Backfill is enabled, but full auto is also enabled. Disabling backfill...")
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(config_instance.current_profile, config)
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
- logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
955
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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(file_path), "gamesentenceminer_old.log")
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("Couldn't rotate log, probably because the file is being written to by another process. NOT AN ERROR")
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): # Avoid infinite loop
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}")