GameSentenceMiner 2.14.8__py3-none-any.whl → 2.14.9__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 (66) hide show
  1. GameSentenceMiner/config_gui.py +19 -10
  2. GameSentenceMiner/gsm.py +68 -8
  3. GameSentenceMiner/locales/en_us.json +4 -0
  4. GameSentenceMiner/locales/ja_jp.json +4 -0
  5. GameSentenceMiner/locales/zh_cn.json +4 -0
  6. GameSentenceMiner/obs.py +12 -8
  7. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/METADATA +1 -2
  8. gamesentenceminer-2.14.9.dist-info/RECORD +24 -0
  9. GameSentenceMiner/ai/__init__.py +0 -0
  10. GameSentenceMiner/ai/ai_prompting.py +0 -473
  11. GameSentenceMiner/ocr/__init__.py +0 -0
  12. GameSentenceMiner/ocr/gsm_ocr_config.py +0 -174
  13. GameSentenceMiner/ocr/ocrconfig.py +0 -129
  14. GameSentenceMiner/ocr/owocr_area_selector.py +0 -629
  15. GameSentenceMiner/ocr/owocr_helper.py +0 -638
  16. GameSentenceMiner/ocr/ss_picker.py +0 -140
  17. GameSentenceMiner/owocr/owocr/__init__.py +0 -1
  18. GameSentenceMiner/owocr/owocr/__main__.py +0 -9
  19. GameSentenceMiner/owocr/owocr/config.py +0 -148
  20. GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -1238
  21. GameSentenceMiner/owocr/owocr/ocr.py +0 -1690
  22. GameSentenceMiner/owocr/owocr/run.py +0 -1817
  23. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -109
  24. GameSentenceMiner/tools/__init__.py +0 -0
  25. GameSentenceMiner/tools/audio_offset_selector.py +0 -215
  26. GameSentenceMiner/tools/ss_selector.py +0 -135
  27. GameSentenceMiner/tools/window_transparency.py +0 -214
  28. GameSentenceMiner/util/__init__.py +0 -0
  29. GameSentenceMiner/util/communication/__init__.py +0 -22
  30. GameSentenceMiner/util/communication/send.py +0 -7
  31. GameSentenceMiner/util/communication/websocket.py +0 -94
  32. GameSentenceMiner/util/configuration.py +0 -1198
  33. GameSentenceMiner/util/db.py +0 -408
  34. GameSentenceMiner/util/downloader/Untitled_json.py +0 -472
  35. GameSentenceMiner/util/downloader/__init__.py +0 -0
  36. GameSentenceMiner/util/downloader/download_tools.py +0 -194
  37. GameSentenceMiner/util/downloader/oneocr_dl.py +0 -250
  38. GameSentenceMiner/util/electron_config.py +0 -259
  39. GameSentenceMiner/util/ffmpeg.py +0 -571
  40. GameSentenceMiner/util/get_overlay_coords.py +0 -366
  41. GameSentenceMiner/util/gsm_utils.py +0 -323
  42. GameSentenceMiner/util/model.py +0 -206
  43. GameSentenceMiner/util/notification.py +0 -147
  44. GameSentenceMiner/util/text_log.py +0 -214
  45. GameSentenceMiner/web/__init__.py +0 -0
  46. GameSentenceMiner/web/service.py +0 -132
  47. GameSentenceMiner/web/static/__init__.py +0 -0
  48. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  49. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  50. GameSentenceMiner/web/static/favicon.ico +0 -0
  51. GameSentenceMiner/web/static/favicon.svg +0 -3
  52. GameSentenceMiner/web/static/site.webmanifest +0 -21
  53. GameSentenceMiner/web/static/style.css +0 -292
  54. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  55. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  56. GameSentenceMiner/web/templates/__init__.py +0 -0
  57. GameSentenceMiner/web/templates/index.html +0 -50
  58. GameSentenceMiner/web/templates/text_replacements.html +0 -238
  59. GameSentenceMiner/web/templates/utility.html +0 -483
  60. GameSentenceMiner/web/texthooking_page.py +0 -584
  61. GameSentenceMiner/wip/__init___.py +0 -0
  62. gamesentenceminer-2.14.8.dist-info/RECORD +0 -77
  63. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/WHEEL +0 -0
  64. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/entry_points.txt +0 -0
  65. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/licenses/LICENSE +0 -0
  66. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/top_level.txt +0 -0
@@ -1,1198 +0,0 @@
1
- import dataclasses
2
- import json
3
- import logging
4
- import os
5
- import shutil
6
- import threading
7
- from dataclasses import dataclass, field
8
- from logging.handlers import RotatingFileHandler
9
- from os.path import expanduser
10
- from sys import platform
11
- from typing import List, Dict
12
- import sys
13
- from enum import Enum
14
-
15
- import toml
16
- from dataclasses_json import dataclass_json
17
-
18
- from importlib import metadata
19
-
20
-
21
-
22
- OFF = 'OFF'
23
- # VOSK = 'VOSK'
24
- SILERO = 'SILERO'
25
- WHISPER = 'WHISPER'
26
- # GROQ = 'GROQ'
27
-
28
- # VOSK_BASE = 'BASE'
29
- # VOSK_SMALL = 'SMALL'
30
-
31
- WHISPER_TINY = 'tiny'
32
- WHISPER_BASE = 'base'
33
- WHISPER_SMALL = 'small'
34
- WHISPER_MEDIUM = 'medium'
35
- WHSIPER_LARGE = 'large'
36
- WHISPER_TURBO = 'turbo'
37
-
38
- AI_GEMINI = 'Gemini'
39
- AI_GROQ = 'Groq'
40
- AI_OPENAI = 'OpenAI'
41
-
42
- INFO = 'INFO'
43
- DEBUG = 'DEBUG'
44
-
45
- DEFAULT_CONFIG = 'Default'
46
-
47
- current_game = ''
48
-
49
- supported_formats = {
50
- 'opus': 'libopus',
51
- 'mp3': 'libmp3lame',
52
- 'ogg': 'libvorbis',
53
- 'aac': 'aac',
54
- 'm4a': 'aac',
55
- }
56
-
57
-
58
- def is_linux():
59
- return platform == 'linux'
60
-
61
-
62
- def is_windows():
63
- return platform == 'win32'
64
-
65
-
66
- class Locale(Enum):
67
- English = 'en_us'
68
- 日本語 = 'ja_jp'
69
- 한국어 = 'ko_kr'
70
- 中文 = 'zh_cn'
71
- Español = 'es_es'
72
- Français = 'fr_fr'
73
- Deutsch = 'de_de'
74
- Italiano = 'it_it'
75
- Русский = 'ru_ru'
76
-
77
- @classmethod
78
- def from_any(cls, value: str) -> 'Locale':
79
- """
80
- Lookup Locale by either enum name (e.g. 'English') or value (e.g. 'en_us').
81
- Case-insensitive.
82
- """
83
- value_lower = value.lower()
84
- for locale in cls:
85
- if locale.name.lower() == value_lower or locale.value.lower() == value_lower:
86
- return locale
87
- raise KeyError(f"Locale '{value}' not found.")
88
-
89
- def __getitem__(cls, item):
90
- try:
91
- return cls.from_any(item)
92
- except KeyError:
93
- raise
94
-
95
-
96
- # Patch Enum's __getitem__ for this class
97
- Locale.__getitem__ = classmethod(Locale.__getitem__)
98
-
99
-
100
- class Language(Enum):
101
- JAPANESE = "ja"
102
- ENGLISH = "en"
103
- KOREAN = "ko"
104
- CHINESE = "zh"
105
- SPANISH = "es"
106
- FRENCH = "fr"
107
- GERMAN = "de"
108
- ITALIAN = "it"
109
- RUSSIAN = "ru"
110
- PORTUGUESE = "pt"
111
- HINDI = "hi"
112
- ARABIC = "ar"
113
- TURKISH = "tr"
114
- DUTCH = "nl"
115
- SWEDISH = "sv"
116
- FINNISH = "fi"
117
- DANISH = "da"
118
- NORWEGIAN = "no"
119
-
120
-
121
- AVAILABLE_LANGUAGES = [lang.value for lang in Language]
122
- AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
123
-
124
-
125
- class CommonLanguages(str, Enum):
126
- """
127
- An Enum of the world's most common languages, based on total speaker count.
128
-
129
- The enum member is the common English name (e.g., ENGLISH) and its
130
- value is the ISO 639-1 two-letter code (e.g., 'en').
131
-
132
- Inheriting from `str` allows for direct comparison and use in functions
133
- that expect a string, e.g., `CommonLanguages.FRENCH == 'fr'`.
134
-
135
- This list is curated from Wikipedia's "List of languages by total number of speakers"
136
- and contains over 200 entries to provide broad but practical coverage.
137
- """
138
- ENGLISH = 'en'
139
- AFRIKAANS = 'af'
140
- AKAN = 'ak'
141
- ALBANIAN = 'sq'
142
- ALGERIAN_SPOKEN_ARABIC = 'arq'
143
- AMHARIC = 'am'
144
- ARMENIAN = 'hy'
145
- ASSAMESE = 'as'
146
- BAMBARA = 'bm'
147
- BASQUE = 'eu'
148
- BELARUSIAN = 'be'
149
- BENGALI = 'bn'
150
- BHOJPURI = 'bho'
151
- BOSNIAN = 'bs'
152
- BODO = 'brx'
153
- BULGARIAN = 'bg'
154
- BURMESE = 'my'
155
- CAPE_VERDEAN_CREOLE = 'kea'
156
- CATALAN = 'ca'
157
- CEBUANO = 'ceb'
158
- CHHATTISGARHI = 'hns'
159
- CHITTAGONIAN = 'ctg'
160
- CROATIAN = 'hr'
161
- CZECH = 'cs'
162
- DANISH = 'da'
163
- DECCAN = 'dcc'
164
- DOGRI = 'doi'
165
- DZONGKHA = 'dz'
166
- DUTCH = 'nl'
167
- EGYPTIAN_SPOKEN_ARABIC = 'arz'
168
- ESTONIAN = 'et'
169
- EWE = 'ee'
170
- FAROESE = 'fo'
171
- FIJIAN = 'fj'
172
- FINNISH = 'fi'
173
- FRENCH = 'fr'
174
- GALICIAN = 'gl'
175
- GAN_CHINESE = 'gan'
176
- GEORGIAN = 'ka'
177
- GERMAN = 'de'
178
- GREEK = 'el'
179
- GREENLANDIC = 'kl'
180
- GUJARATI = 'gu'
181
- HAITIAN_CREOLE = 'ht'
182
- HAUSA = 'ha'
183
- HAKKA_CHINESE = 'hak'
184
- HARYANVI = 'bgc'
185
- HEBREW = 'he'
186
- HINDI = 'hi'
187
- HUNGARIAN = 'hu'
188
- ICELANDIC = 'is'
189
- IGBO = 'ig'
190
- INDONESIAN = 'id'
191
- IRANIAN_PERSIAN = 'fa'
192
- IRISH = 'ga'
193
- ITALIAN = 'it'
194
- JAVANESE = 'jv'
195
- JAMAICAN_PATOIS = 'jam'
196
- JAPANESE = 'ja'
197
- KANNADA = 'kn'
198
- KASHMIRI = 'ks'
199
- KAZAKH = 'kk'
200
- KHMER = 'km'
201
- KONGO = 'kg'
202
- KONKANI = 'kok'
203
- KOREAN = 'ko'
204
- KURDISH = 'kmr'
205
- LAO = 'lo'
206
- LATVIAN = 'lv'
207
- LINGALA = 'ln'
208
- LITHUANIAN = 'lt'
209
- LUBA_KASAI = 'lua'
210
- LUXEMBOURGISH = 'lb'
211
- MACEDONIAN = 'mk'
212
- MADURESE = 'mad'
213
- MAGAHI = 'mag'
214
- MAITHILI = 'mai'
215
- MALAGASY = 'mg'
216
- MALAYALAM = 'ml'
217
- MALTESE = 'mt'
218
- MANDARIN_CHINESE = 'zh'
219
- MANIPURI = 'mni'
220
- MARATHI = 'mr'
221
- MAORI = 'mi'
222
- MAURITIAN_CREOLE = 'mfe'
223
- MIN_NAN_CHINESE = 'nan'
224
- MINANGKABAU = 'min'
225
- MONGOLIAN = 'mn'
226
- MONTENEGRIN = 'cnr'
227
- MOROCCAN_SPOKEN_ARABIC = 'ary'
228
- NDEBELE = 'nr'
229
- NEPALI = 'ne'
230
- NIGERIAN_PIDGIN = 'pcm'
231
- NORTHERN_KURDISH = 'kmr'
232
- NORTHERN_PASHTO = 'pbu'
233
- NORTHERN_UZBEK = 'uz'
234
- NORWEGIAN = 'no'
235
- ODIA = 'or'
236
- PAPIAMENTO = 'pap'
237
- POLISH = 'pl'
238
- PORTUGUESE = 'pt'
239
- ROMANIAN = 'ro'
240
- RWANDA = 'rw'
241
- RUSSIAN = 'ru'
242
- SAMOAN = 'sm'
243
- SANTALI = 'sat'
244
- SARAIKI = 'skr'
245
- SCOTTISH_GAELIC = 'gd'
246
- SEYCHELLOIS_CREOLE = 'crs'
247
- SERBIAN = 'sr'
248
- SHONA = 'sn'
249
- SINDHI = 'sd'
250
- SINHALA = 'si'
251
- SLOVAK = 'sk'
252
- SLOVENIAN = 'sl'
253
- SOMALI = 'so'
254
- SOTHO = 'st'
255
- SOUTH_AZERBAIJANI = 'azb'
256
- SOUTHERN_PASHTO = 'ps'
257
- SPANISH = 'es'
258
- STANDARD_ARABIC = 'ar'
259
- SUDANESE_SPOKEN_ARABIC = 'apd'
260
- SUNDANESE = 'su'
261
- SWAHILI = 'sw'
262
- SWATI = 'ss'
263
- SWEDISH = 'sv'
264
- SYLHETI = 'syl'
265
- TAGALOG = 'tl'
266
- TAMIL = 'ta'
267
- TELUGU = 'te'
268
- THAI = 'th'
269
- TIGRINYA = 'ti'
270
- TIBETAN = 'bo'
271
- TONGAN = 'to'
272
- TSONGA = 'ts'
273
- TSWANA = 'tn'
274
- TWI = 'twi'
275
- UKRAINIAN = 'uk'
276
- URDU = 'ur'
277
- UYGHUR = 'ug'
278
- VENDA = 've'
279
- VIETNAMESE = 'vi'
280
- WELSH = 'cy'
281
- WESTERN_PUNJABI = 'pnb'
282
- WOLOF = 'wo'
283
- WU_CHINESE = 'wuu'
284
- XHOSA = 'xh'
285
- YORUBA = 'yo'
286
- YUE_CHINESE = 'yue'
287
- ZULU = 'zu'
288
-
289
- # Helper methods
290
-
291
- @classmethod
292
- def get_all_codes(cls) -> list[str]:
293
- """Returns a list of all language codes (e.g., ['en', 'zh', 'hi'])."""
294
- return [lang.value for lang in cls]
295
-
296
- @classmethod
297
- def get_all_names(cls) -> list[str]:
298
- """Returns a list of all language names (e.g., ['ENGLISH', 'MANDARIN_CHINESE'])."""
299
- return [lang.name for lang in cls]
300
-
301
- @classmethod
302
- def get_all_names_pretty(cls) -> list[str]:
303
- """Returns a list of all language names formatted for display (e.g., ['English', 'Mandarin Chinese'])."""
304
- return [lang.name.replace('_', ' ').title() for lang in cls]
305
-
306
- @classmethod
307
- def get_choices(cls) -> list[tuple[str, str]]:
308
- """
309
- Returns a list of (value, label) tuples for use in web framework
310
- choice fields (e.g., Django, Flask).
311
-
312
- Example: [('en', 'English'), ('zh', 'Mandarin Chinese')]
313
- """
314
- return [(lang.value, lang.name.replace('_', ' ').title()) for lang in cls]
315
-
316
- # Method to lookup language by it's name
317
- @classmethod
318
- def from_name(cls, name: str) -> 'CommonLanguages':
319
- """
320
- Looks up a language by its name (e.g., 'ENGLISH') and returns the corresponding enum member.
321
- Raises ValueError if not found.
322
- """
323
- try:
324
- return cls[name.upper()]
325
- except KeyError:
326
- raise ValueError(f"Language '{name}' not found in CommonLanguages")
327
-
328
- # Method to lookup language by its code
329
- @classmethod
330
- def from_code(cls, code: str) -> 'CommonLanguages':
331
- """
332
- Looks up a language by its code (e.g., 'en') and returns the corresponding enum member.
333
- Raises ValueError if not found.
334
- """
335
- for lang in cls:
336
- if lang.value == code:
337
- return lang
338
- raise ValueError(
339
- f"Language code '{code}' not found in CommonLanguages")
340
-
341
- @classmethod
342
- def name_from_code(cls, code: str) -> str:
343
- """
344
- Returns the name of the language given its code (e.g., 'en' -> 'ENGLISH').
345
- Raises ValueError if not found.
346
- """
347
- return cls.from_code(code).name
348
-
349
-
350
- PACKAGE_NAME = "GameSentenceMiner"
351
-
352
-
353
- def get_current_version():
354
- try:
355
- version = metadata.version(PACKAGE_NAME)
356
- return version
357
- except metadata.PackageNotFoundError:
358
- return None
359
-
360
-
361
- def get_latest_version():
362
- try:
363
- import requests
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
-
388
- @dataclass_json
389
- @dataclass
390
- class General:
391
- use_websocket: bool = True
392
- use_clipboard: bool = True
393
- use_both_clipboard_and_websocket: bool = False
394
- merge_matching_sequential_text: bool = False
395
- websocket_uri: str = 'localhost:6677,localhost:9001,localhost:2333'
396
- open_config_on_startup: bool = False
397
- open_multimine_on_startup: bool = True
398
- texthook_replacement_regex: str = ""
399
- texthooker_port: int = 55000
400
- native_language: str = CommonLanguages.ENGLISH.value
401
-
402
- def get_native_language_name(self) -> str:
403
- try:
404
- return CommonLanguages.name_from_code(self.native_language)
405
- except ValueError:
406
- return "Unknown"
407
-
408
-
409
- @dataclass_json
410
- @dataclass
411
- class Paths:
412
- folder_to_watch: str = expanduser("~/Videos/GSM")
413
- output_folder: str = expanduser("~/Videos/GSM/Output")
414
- copy_temp_files_to_output_folder: bool = False
415
- open_output_folder_on_card_creation: bool = False
416
- copy_trimmed_replay_to_output_folder: bool = False
417
- remove_video: bool = True
418
- remove_audio: bool = False
419
- remove_screenshot: bool = False
420
-
421
- def __post_init__(self):
422
- if self.folder_to_watch:
423
- self.folder_to_watch = os.path.normpath(self.folder_to_watch)
424
- if self.output_folder:
425
- self.output_folder = os.path.normpath(self.output_folder)
426
-
427
-
428
- @dataclass_json
429
- @dataclass
430
- class Anki:
431
- update_anki: bool = True
432
- url: str = 'http://127.0.0.1:8765'
433
- sentence_field: str = "Sentence"
434
- sentence_audio_field: str = "SentenceAudio"
435
- picture_field: str = "Picture"
436
- word_field: str = 'Expression'
437
- previous_sentence_field: str = ''
438
- previous_image_field: str = ''
439
- # Initialize to None and set it in __post_init__
440
- custom_tags: List[str] = None
441
- tags_to_check: List[str] = None
442
- add_game_tag: bool = True
443
- polling_rate: int = 200
444
- overwrite_audio: bool = False
445
- overwrite_picture: bool = True
446
- multi_overwrites_sentence: bool = True
447
- parent_tag: str = "Game"
448
-
449
- def __post_init__(self):
450
- if self.custom_tags is None:
451
- self.custom_tags = ['GSM']
452
- if self.tags_to_check is None:
453
- self.tags_to_check = []
454
-
455
-
456
- @dataclass_json
457
- @dataclass
458
- class Features:
459
- full_auto: bool = True
460
- notify_on_update: bool = True
461
- open_anki_edit: bool = False
462
- open_anki_in_browser: bool = True
463
- browser_query: str = ''
464
- backfill_audio: bool = False
465
-
466
-
467
- @dataclass_json
468
- @dataclass
469
- class Screenshot:
470
- enabled: bool = True
471
- width: str = 0
472
- height: str = 0
473
- quality: str = 85
474
- extension: str = "webp"
475
- custom_ffmpeg_settings: str = ''
476
- custom_ffmpeg_option_selected: str = ''
477
- screenshot_hotkey_updates_anki: bool = False
478
- seconds_after_line: float = 1.0
479
- use_beginning_of_line_as_screenshot: bool = True
480
- use_new_screenshot_logic: bool = False
481
- screenshot_timing_setting: str = 'beginning' # 'middle', 'end'
482
- use_screenshot_selector: bool = False
483
-
484
- def __post_init__(self):
485
- if not self.screenshot_timing_setting and self.use_beginning_of_line_as_screenshot:
486
- self.screenshot_timing_setting = 'beginning'
487
- if not self.screenshot_timing_setting and self.use_new_screenshot_logic:
488
- self.screenshot_timing_setting = 'middle'
489
- if not self.screenshot_timing_setting and not self.use_beginning_of_line_as_screenshot and not self.use_new_screenshot_logic:
490
- self.screenshot_timing_setting = 'end'
491
- if self.width and self.height == 0:
492
- self.height = -1
493
- if self.width == 0 and self.height:
494
- self.width = -1
495
-
496
-
497
- @dataclass_json
498
- @dataclass
499
- class Audio:
500
- enabled: bool = True
501
- extension: str = 'opus'
502
- beginning_offset: float = -0.5
503
- end_offset: float = 0.5
504
- pre_vad_end_offset: float = 0.0
505
- ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
506
- ffmpeg_reencode_options_to_use: str = ''
507
- external_tool: str = ""
508
- anki_media_collection: str = ""
509
- external_tool_enabled: bool = True
510
- custom_encode_settings: str = ''
511
-
512
- def __post_init__(self):
513
- self.ffmpeg_reencode_options_to_use = self.ffmpeg_reencode_options.replace(
514
- "{format}", self.extension).replace("{encoder}", supported_formats.get(self.extension, ''))
515
- if not self.anki_media_collection:
516
- self.anki_media_collection = get_default_anki_media_collection_path()
517
- if self.anki_media_collection:
518
- self.anki_media_collection = os.path.normpath(
519
- self.anki_media_collection)
520
- if self.external_tool:
521
- self.external_tool = os.path.normpath(self.external_tool)
522
-
523
-
524
- @dataclass_json
525
- @dataclass
526
- class OBS:
527
- open_obs: bool = True
528
- close_obs: bool = True
529
- host: str = "127.0.0.1"
530
- port: int = 7274
531
- password: str = "your_password"
532
- get_game_from_scene: bool = True
533
- minimum_replay_size: int = 0
534
-
535
-
536
- @dataclass_json
537
- @dataclass
538
- class Hotkeys:
539
- reset_line: str = 'f5'
540
- take_screenshot: str = 'f6'
541
- open_utility: str = 'ctrl+m'
542
- play_latest_audio: str = 'f7'
543
-
544
-
545
- @dataclass_json
546
- @dataclass
547
- class VAD:
548
- whisper_model: str = WHISPER_BASE
549
- do_vad_postprocessing: bool = True
550
- language: str = 'ja'
551
- # vosk_url: str = VOSK_BASE
552
- selected_vad_model: str = WHISPER
553
- backup_vad_model: str = SILERO
554
- trim_beginning: bool = False
555
- beginning_offset: float = -0.25
556
- add_audio_on_no_results: bool = False
557
- cut_and_splice_segments: bool = False
558
- splice_padding: float = 0.1
559
-
560
- def is_silero(self):
561
- return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
562
-
563
- def is_whisper(self):
564
- return self.selected_vad_model == WHISPER or self.backup_vad_model == WHISPER
565
-
566
- # def is_vosk(self):
567
- # return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
568
-
569
- # def is_groq(self):
570
- # return self.selected_vad_model == GROQ or self.backup_vad_model == GROQ
571
-
572
-
573
- @dataclass_json
574
- @dataclass
575
- class Advanced:
576
- plaintext_websocket_port: int = -1
577
- audio_player_path: str = ''
578
- video_player_path: str = ''
579
- show_screenshot_buttons: bool = False
580
- multi_line_line_break: str = '<br>'
581
- multi_line_sentence_storage_field: str = ''
582
- ocr_websocket_port: int = 9002
583
- texthooker_communication_websocket_port: int = 55001
584
-
585
- def __post_init__(self):
586
- if self.plaintext_websocket_port == -1:
587
- self.plaintext_websocket_port = self.texthooker_communication_websocket_port + 1
588
-
589
-
590
- @dataclass_json
591
- @dataclass
592
- class Ai:
593
- enabled: bool = False
594
- anki_field: str = ''
595
- provider: str = AI_GEMINI
596
- gemini_model: str = 'gemini-2.5-flash-lite'
597
- groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
598
- gemini_api_key: str = ''
599
- api_key: str = '' # Legacy support, will be moved to gemini_api_key if provider is gemini
600
- groq_api_key: str = ''
601
- open_ai_url: str = ''
602
- open_ai_model: str = ''
603
- open_ai_api_key: str = ''
604
- use_canned_translation_prompt: bool = True
605
- use_canned_context_prompt: bool = False
606
- custom_prompt: str = ''
607
- dialogue_context_length: int = 10
608
-
609
- def __post_init__(self):
610
- if not self.gemini_api_key:
611
- self.gemini_api_key = self.api_key
612
- if self.provider == 'gemini':
613
- self.provider = AI_GEMINI
614
- if self.provider == 'groq':
615
- self.provider = AI_GROQ
616
- if self.gemini_model in ['RECOMMENDED', 'OTHER']:
617
- self.gemini_model = 'gemini-2.5-flash-lite'
618
- if self.groq_model in ['RECOMMENDED', 'OTHER']:
619
- self.groq_model = 'meta-llama/llama-4-scout-17b-16e-instruct'
620
-
621
- # Change Legacy Model Name
622
- if self.gemini_model == 'gemini-2.5-flash-lite-preview-06-17':
623
- self.gemini_model = 'gemini-2.5-flash-lite'
624
-
625
-
626
- @dataclass_json
627
- @dataclass
628
- class Overlay:
629
- websocket_port: int = 55499
630
- monitor_to_capture: int = 0
631
-
632
- def __post_init__(self):
633
- if self.monitor_to_capture == -1:
634
- self.monitor_to_capture = 0 # Default to the first monitor if not set
635
-
636
-
637
- @dataclass_json
638
- @dataclass
639
- class WIP:
640
- pass
641
-
642
-
643
- @dataclass_json
644
- @dataclass
645
- class ProfileConfig:
646
- name: str = 'Default'
647
- scenes: List[str] = field(default_factory=list)
648
- general: General = field(default_factory=General)
649
- paths: Paths = field(default_factory=Paths)
650
- anki: Anki = field(default_factory=Anki)
651
- features: Features = field(default_factory=Features)
652
- screenshot: Screenshot = field(default_factory=Screenshot)
653
- audio: Audio = field(default_factory=Audio)
654
- obs: OBS = field(default_factory=OBS)
655
- hotkeys: Hotkeys = field(default_factory=Hotkeys)
656
- vad: VAD = field(default_factory=VAD)
657
- advanced: Advanced = field(default_factory=Advanced)
658
- ai: Ai = field(default_factory=Ai)
659
- overlay: Overlay = field(default_factory=Overlay)
660
- wip: WIP = field(default_factory=WIP)
661
-
662
- def get_field_value(self, section: str, field_name: str):
663
- section_obj = getattr(self, section, None)
664
- if section_obj and hasattr(section_obj, field_name):
665
- return getattr(section_obj, field_name)
666
- else:
667
- raise ValueError(
668
- f"Field '{field_name}' not found in section '{section}' of ProfileConfig.")
669
-
670
- # This is just for legacy support
671
- def load_from_toml(self, file_path: str):
672
- with open(file_path, 'r') as f:
673
- config_data = toml.load(f)
674
-
675
- self.paths.folder_to_watch = expanduser(config_data['paths'].get(
676
- 'folder_to_watch', self.paths.folder_to_watch))
677
-
678
- self.anki.url = config_data['anki'].get('url', self.anki.url)
679
- self.anki.sentence_field = config_data['anki'].get(
680
- 'sentence_field', self.anki.sentence_field)
681
- self.anki.sentence_audio_field = config_data['anki'].get(
682
- 'sentence_audio_field', self.anki.sentence_audio_field)
683
- self.anki.word_field = config_data['anki'].get(
684
- 'word_field', self.anki.word_field)
685
- self.anki.picture_field = config_data['anki'].get(
686
- 'picture_field', self.anki.picture_field)
687
- self.anki.custom_tags = config_data['anki'].get(
688
- 'custom_tags', self.anki.custom_tags)
689
- self.anki.add_game_tag = config_data['anki'].get(
690
- 'add_game_tag', self.anki.add_game_tag)
691
- self.anki.polling_rate = config_data['anki'].get(
692
- 'polling_rate', self.anki.polling_rate)
693
- self.anki.overwrite_audio = config_data['anki_overwrites'].get(
694
- 'overwrite_audio', self.anki.overwrite_audio)
695
- self.anki.overwrite_picture = config_data['anki_overwrites'].get('overwrite_picture',
696
- self.anki.overwrite_picture)
697
-
698
- self.features.full_auto = config_data['features'].get(
699
- 'do_vosk_postprocessing', self.features.full_auto)
700
- self.features.notify_on_update = config_data['features'].get(
701
- 'notify_on_update', self.features.notify_on_update)
702
- self.features.open_anki_edit = config_data['features'].get(
703
- 'open_anki_edit', self.features.open_anki_edit)
704
- self.features.backfill_audio = config_data['features'].get(
705
- 'backfill_audio', self.features.backfill_audio)
706
-
707
- self.screenshot.width = config_data['screenshot'].get(
708
- 'width', self.screenshot.width)
709
- self.screenshot.height = config_data['screenshot'].get(
710
- 'height', self.screenshot.height)
711
- self.screenshot.quality = config_data['screenshot'].get(
712
- 'quality', self.screenshot.quality)
713
- self.screenshot.extension = config_data['screenshot'].get(
714
- 'extension', self.screenshot.extension)
715
- self.screenshot.custom_ffmpeg_settings = config_data['screenshot'].get('custom_ffmpeg_settings',
716
- self.screenshot.custom_ffmpeg_settings)
717
-
718
- self.audio.extension = config_data['audio'].get(
719
- 'extension', self.audio.extension)
720
- self.audio.beginning_offset = config_data['audio'].get(
721
- 'beginning_offset', self.audio.beginning_offset)
722
- self.audio.end_offset = config_data['audio'].get(
723
- 'end_offset', self.audio.end_offset)
724
- self.audio.ffmpeg_reencode_options = config_data['audio'].get('ffmpeg_reencode_options',
725
- self.audio.ffmpeg_reencode_options)
726
-
727
- self.vad.whisper_model = config_data['vosk'].get(
728
- 'whisper_model', self.vad.whisper_model)
729
- self.vad.vosk_url = config_data['vosk'].get('url', self.vad.vosk_url)
730
- self.vad.do_vad_postprocessing = config_data['features'].get('do_vosk_postprocessing',
731
- self.vad.do_vad_postprocessing)
732
- self.vad.trim_beginning = config_data['audio'].get(
733
- 'vosk_trim_beginning', self.vad.trim_beginning)
734
-
735
- self.obs.host = config_data['obs'].get('host', self.obs.host)
736
- self.obs.port = config_data['obs'].get('port', self.obs.port)
737
- self.obs.password = config_data['obs'].get(
738
- 'password', self.obs.password)
739
-
740
- self.general.use_websocket = config_data['websocket'].get(
741
- 'enabled', self.general.use_websocket)
742
- self.general.websocket_uri = config_data['websocket'].get(
743
- 'uri', self.general.websocket_uri)
744
-
745
- self.hotkeys.reset_line = config_data['hotkeys'].get(
746
- 'reset_line', self.hotkeys.reset_line)
747
- self.hotkeys.take_screenshot = config_data['hotkeys'].get(
748
- 'take_screenshot', self.hotkeys.take_screenshot)
749
-
750
- with open(get_config_path(), 'w') as f:
751
- f.write(self.to_json(indent=4))
752
- print(
753
- 'config.json successfully generated from previous settings. config.toml will no longer be used.')
754
-
755
- return self
756
-
757
- def restart_required(self, previous):
758
- previous: ProfileConfig
759
- if any([previous.paths.folder_to_watch != self.paths.folder_to_watch,
760
- previous.obs.open_obs != self.obs.open_obs,
761
- previous.obs.host != self.obs.host,
762
- previous.obs.port != self.obs.port
763
- ]):
764
- logger.info("Restart Required for Some Settings that were Changed")
765
- return True
766
- return False
767
-
768
- def config_changed(self, new: 'ProfileConfig') -> bool:
769
- return self != new
770
-
771
-
772
- @dataclass_json
773
- @dataclass
774
- class Config:
775
- configs: Dict[str, ProfileConfig] = field(default_factory=dict)
776
- current_profile: str = DEFAULT_CONFIG
777
- switch_to_default_if_not_found: bool = True
778
- locale: str = Locale.English.value
779
-
780
- @classmethod
781
- def new(cls):
782
- instance = cls(
783
- configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
784
- return instance
785
-
786
- def get_locale(self) -> Locale:
787
- try:
788
- return Locale.from_any(self.locale)
789
- except KeyError:
790
- logger.warning(
791
- f"Locale '{self.locale}' not found. Defaulting to English.")
792
- return Locale.English
793
-
794
- @classmethod
795
- def load(cls):
796
- config_path = get_config_path()
797
- if os.path.exists(config_path):
798
- with open(config_path, 'r') as file:
799
- data = json.load(file)
800
- return cls.from_dict(data)
801
- else:
802
- return cls.new()
803
-
804
- def save(self):
805
- with open(get_config_path(), 'w') as file:
806
- json.dump(self.to_dict(), file, indent=4)
807
- return self
808
-
809
- def get_config(self) -> ProfileConfig:
810
- if self.current_profile not in self.configs:
811
- logger.warning(
812
- f"Profile '{self.current_profile}' not found. Switching to default profile.")
813
- self.current_profile = DEFAULT_CONFIG
814
- return self.configs[self.current_profile]
815
-
816
- def set_config_for_profile(self, profile: str, config: ProfileConfig):
817
- config.name = profile
818
- self.configs[profile] = config
819
-
820
- def has_config_for_current_game(self):
821
- return current_game in self.configs
822
-
823
- def get_all_profile_names(self):
824
- return list(self.configs.keys())
825
-
826
- def get_default_config(self):
827
- return self.configs[DEFAULT_CONFIG]
828
-
829
- def sync_changed_fields(self, previous_config: ProfileConfig):
830
- current_config = self.get_config()
831
-
832
- for section in current_config.to_dict():
833
- if dataclasses.is_dataclass(getattr(current_config, section, None)):
834
- for field_name in getattr(current_config, section, None).to_dict():
835
- config_section = getattr(current_config, section, None)
836
- previous_config_section = getattr(
837
- previous_config, section, None)
838
- current_value = getattr(config_section, field_name, None)
839
- previous_value = getattr(
840
- previous_config_section, field_name, None)
841
- if str(current_value).strip() != str(previous_value).strip():
842
- logger.info(
843
- f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
844
- for profile in self.configs.values():
845
- if profile != current_config:
846
- profile_section = getattr(
847
- profile, section, None)
848
- if profile_section:
849
- setattr(profile_section,
850
- field_name, current_value)
851
- logger.info(
852
- f"Updated '{field_name}' in profile '{profile.name}'")
853
-
854
- return self
855
-
856
- def sync_shared_fields(self):
857
- config = self.get_config()
858
- for profile in self.configs.values():
859
- self.sync_shared_field(
860
- config.hotkeys, profile.hotkeys, "reset_line")
861
- self.sync_shared_field(
862
- config.hotkeys, profile.hotkeys, "take_screenshot")
863
- self.sync_shared_field(
864
- config.hotkeys, profile.hotkeys, "open_utility")
865
- self.sync_shared_field(
866
- config.hotkeys, profile.hotkeys, "play_latest_audio")
867
- self.sync_shared_field(config.anki, profile.anki, "url")
868
- self.sync_shared_field(config.anki, profile.anki, "sentence_field")
869
- self.sync_shared_field(
870
- config.anki, profile.anki, "sentence_audio_field")
871
- self.sync_shared_field(config.anki, profile.anki, "picture_field")
872
- self.sync_shared_field(config.anki, profile.anki, "word_field")
873
- self.sync_shared_field(
874
- config.anki, profile.anki, "previous_sentence_field")
875
- self.sync_shared_field(
876
- config.anki, profile.anki, "previous_image_field")
877
- self.sync_shared_field(config.anki, profile.anki, "tags_to_check")
878
- self.sync_shared_field(config.anki, profile.anki, "add_game_tag")
879
- self.sync_shared_field(config.anki, profile.anki, "polling_rate")
880
- self.sync_shared_field(
881
- config.anki, profile.anki, "overwrite_audio")
882
- self.sync_shared_field(
883
- config.anki, profile.anki, "overwrite_picture")
884
- self.sync_shared_field(
885
- config.anki, profile.anki, "multi_overwrites_sentence")
886
- self.sync_shared_field(
887
- config.general, profile.general, "open_config_on_startup")
888
- self.sync_shared_field(
889
- config.general, profile.general, "open_multimine_on_startup")
890
- self.sync_shared_field(
891
- config.general, profile.general, "websocket_uri")
892
- self.sync_shared_field(
893
- config.general, profile.general, "texthooker_port")
894
- self.sync_shared_field(
895
- config.audio, profile.audio, "external_tool")
896
- self.sync_shared_field(
897
- config.audio, profile.audio, "anki_media_collection")
898
- self.sync_shared_field(
899
- config.audio, profile.audio, "external_tool_enabled")
900
- self.sync_shared_field(
901
- config.audio, profile.audio, "custom_encode_settings")
902
- self.sync_shared_field(
903
- config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
904
- self.sync_shared_field(config, profile, "advanced")
905
- self.sync_shared_field(config, profile, "paths")
906
- self.sync_shared_field(config, profile, "obs")
907
- self.sync_shared_field(config, profile, "wip")
908
- self.sync_shared_field(config.ai, profile.ai, "anki_field")
909
- self.sync_shared_field(config.ai, profile.ai, "provider")
910
- self.sync_shared_field(config.ai, profile.ai, "api_key")
911
- self.sync_shared_field(config.ai, profile.ai, "gemini_api_key")
912
- self.sync_shared_field(config.ai, profile.ai, "groq_api_key")
913
-
914
- return self
915
-
916
- def sync_shared_field(self, config, config2, field_name):
917
- try:
918
- config_value = getattr(config, field_name, None)
919
- config2_value = getattr(config2, field_name, None)
920
-
921
- if config_value != config2_value: # Check if values are different.
922
- if config_value is not None:
923
- logging.info(
924
- f"Syncing shared field '{field_name}' to other profile.")
925
- setattr(config2, field_name, config_value)
926
- elif config2_value is not None:
927
- logging.info(
928
- f"Syncing shared field '{field_name}' to current profile.")
929
- setattr(config, field_name, config2_value)
930
- except AttributeError as e:
931
- logging.error(f"AttributeError during sync of '{field_name}': {e}")
932
- except Exception as e:
933
- logging.error(
934
- f"An unexpected error occurred during sync of '{field_name}': {e}")
935
-
936
-
937
- def get_default_anki_path():
938
- if platform == 'win32': # Windows
939
- base_dir = os.getenv('APPDATA')
940
- else: # macOS and Linux
941
- base_dir = '~/.local/share/'
942
- config_dir = os.path.join(base_dir, 'Anki2')
943
- return config_dir
944
-
945
-
946
- def get_default_anki_media_collection_path():
947
- return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
948
-
949
-
950
- def get_app_directory():
951
- if platform == 'win32': # Windows
952
- appdata_dir = os.getenv('APPDATA')
953
- else: # macOS and Linux
954
- appdata_dir = os.path.expanduser('~/.config')
955
- config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
956
- # Create the directory if it doesn't exist
957
- os.makedirs(config_dir, exist_ok=True)
958
- return config_dir
959
-
960
-
961
- def get_log_path():
962
- path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
963
- os.makedirs(os.path.dirname(path), exist_ok=True)
964
- return path
965
-
966
-
967
- temp_directory = ''
968
-
969
-
970
- def get_temporary_directory(delete=False):
971
- global temp_directory
972
- if not temp_directory:
973
- temp_directory = os.path.join(get_app_directory(), 'temp')
974
- os.makedirs(temp_directory, exist_ok=True)
975
- if delete:
976
- for filename in os.listdir(temp_directory):
977
- file_path = os.path.join(temp_directory, filename)
978
- try:
979
- if os.path.isfile(file_path) or os.path.islink(file_path):
980
- os.unlink(file_path)
981
- elif os.path.isdir(file_path):
982
- shutil.rmtree(file_path)
983
- except Exception as e:
984
- logger.error(f"Failed to delete {file_path}. Reason: {e}")
985
- return temp_directory
986
-
987
-
988
- def get_config_path():
989
- return os.path.join(get_app_directory(), 'config.json')
990
-
991
-
992
- def load_config():
993
- config_path = get_config_path()
994
-
995
- if os.path.exists('config.json') and not os.path.exists(config_path):
996
- shutil.copy('config.json', config_path)
997
-
998
- if os.path.exists(config_path):
999
- try:
1000
- with open(config_path, 'r') as file:
1001
- config_file = json.load(file)
1002
- if "current_profile" in config_file:
1003
- return Config.from_dict(config_file)
1004
- else:
1005
- print(f"Loading Profile-less Config, Converting to new Config!")
1006
- with open(config_path, 'r') as file:
1007
- config_file = json.load(file)
1008
-
1009
- config = ProfileConfig.from_dict(config_file)
1010
- new_config = Config(
1011
- configs={DEFAULT_CONFIG: config}, current_profile=DEFAULT_CONFIG)
1012
-
1013
- config.save()
1014
- return new_config
1015
- except json.JSONDecodeError as e:
1016
- logger.error(
1017
- f"Error parsing config.json, saving backup and returning new config: {e}")
1018
- shutil.copy(config_path, config_path + '.bak')
1019
- config = Config.new()
1020
- config.save()
1021
- return config
1022
- elif os.path.exists('config.toml'):
1023
- config = ProfileConfig().load_from_toml('config.toml')
1024
- new_config = Config({DEFAULT_CONFIG: config},
1025
- current_profile=DEFAULT_CONFIG)
1026
- return new_config
1027
- else:
1028
- config = Config.new()
1029
- config.save()
1030
- return config
1031
-
1032
-
1033
- config_instance: Config = None
1034
-
1035
-
1036
- def get_config():
1037
- global config_instance
1038
- if config_instance is None:
1039
- config_instance = load_config()
1040
- config = config_instance.get_config()
1041
-
1042
- if config.features.backfill_audio and config.features.full_auto:
1043
- logger.warning(
1044
- "Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
1045
- config.features.backfill_audio = False
1046
-
1047
- # print(config_instance.get_config())
1048
- return config_instance.get_config()
1049
-
1050
-
1051
- def reload_config():
1052
- global config_instance
1053
- config_instance = load_config()
1054
- config = config_instance.get_config()
1055
-
1056
- if config.features.backfill_audio and config.features.full_auto:
1057
- logger.warning(
1058
- "Backfill is enabled, but full auto is also enabled. Disabling backfill...")
1059
- config.features.backfill_audio = False
1060
-
1061
-
1062
- def get_master_config():
1063
- return config_instance
1064
-
1065
-
1066
- def save_full_config(config):
1067
- with open(get_config_path(), 'w') as file:
1068
- json.dump(config.to_dict(), file, indent=4)
1069
-
1070
-
1071
- def save_current_config(config):
1072
- global config_instance
1073
- config_instance.set_config_for_profile(
1074
- config_instance.current_profile, config)
1075
- save_full_config(config_instance)
1076
-
1077
-
1078
- def switch_profile_and_save(profile_name):
1079
- global config_instance
1080
- config_instance.current_profile = profile_name
1081
- save_full_config(config_instance)
1082
- return config_instance.get_config()
1083
-
1084
-
1085
- sys.stdout.reconfigure(encoding='utf-8')
1086
- sys.stderr.reconfigure(encoding='utf-8')
1087
-
1088
- logger = logging.getLogger("GameSentenceMiner")
1089
- # Set the base level to DEBUG so that all messages are captured
1090
- logger.setLevel(logging.DEBUG)
1091
- formatter = logging.Formatter(
1092
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
1093
-
1094
- # Create console handler with level INFO
1095
- console_handler = logging.StreamHandler(sys.stdout)
1096
- console_handler.setLevel(logging.INFO)
1097
-
1098
- console_handler.setFormatter(formatter)
1099
-
1100
- logger.addHandler(console_handler)
1101
-
1102
- file_path = get_log_path()
1103
- try:
1104
- if os.path.exists(file_path) and os.path.getsize(file_path) > 1 * 1024 * 1024 and os.access(file_path, os.W_OK):
1105
- old_log_path = os.path.join(os.path.dirname(
1106
- file_path), "gamesentenceminer_old.log")
1107
- if os.path.exists(old_log_path):
1108
- os.remove(old_log_path)
1109
- shutil.move(file_path, old_log_path)
1110
- except Exception as e:
1111
- logger.info(
1112
- "Couldn't rotate log, probably because the file is being written to by another process. NOT AN ERROR")
1113
-
1114
- file_handler = logging.FileHandler(file_path, encoding='utf-8')
1115
- file_handler.setLevel(logging.DEBUG)
1116
- file_handler.setFormatter(formatter)
1117
- logger.addHandler(file_handler)
1118
-
1119
- DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
1120
-
1121
-
1122
- class GsmAppState:
1123
- def __init__(self):
1124
- self.line_for_audio = None
1125
- self.line_for_screenshot = None
1126
- self.anki_note_for_screenshot = None
1127
- self.previous_line_for_audio = None
1128
- self.previous_line_for_screenshot = None
1129
- self.previous_trim_args = None
1130
- self.previous_audio = None
1131
- self.previous_screenshot = None
1132
- self.previous_replay = None
1133
- self.lock = threading.Lock()
1134
- self.last_mined_line = None
1135
- self.keep_running = True
1136
- self.current_game = ''
1137
- self.videos_to_remove = set()
1138
-
1139
-
1140
- @dataclass_json
1141
- @dataclass
1142
- class AnkiUpdateResult:
1143
- success: bool = False
1144
- audio_in_anki: str = ''
1145
- screenshot_in_anki: str = ''
1146
- prev_screenshot_in_anki: str = ''
1147
- sentence_in_anki: str = ''
1148
- multi_line: bool = False
1149
-
1150
- @staticmethod
1151
- def failure():
1152
- return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False)
1153
-
1154
-
1155
- @dataclass_json
1156
- @dataclass
1157
- class GsmStatus:
1158
- ready: bool = False
1159
- status: bool = "Initializing"
1160
- cards_created: int = 0
1161
- websockets_connected: List[str] = field(default_factory=list)
1162
- obs_connected: bool = False
1163
- anki_connected: bool = False
1164
- last_line_received: str = None
1165
- words_being_processed: List[str] = field(default_factory=list)
1166
- clipboard_enabled: bool = True
1167
-
1168
- def add_word_being_processed(self, word: str):
1169
- if word not in self.words_being_processed:
1170
- self.words_being_processed.append(word)
1171
-
1172
- def remove_word_being_processed(self, word: str):
1173
- if word in self.words_being_processed:
1174
- self.words_being_processed.remove(word)
1175
-
1176
-
1177
- def is_running_from_source():
1178
- # Check for .git directory at the project root
1179
- current_dir = os.path.dirname(os.path.abspath(__file__))
1180
- project_root = current_dir
1181
- while project_root != os.path.dirname(project_root): # Avoid infinite loop
1182
- if os.path.isdir(os.path.join(project_root, '.git')):
1183
- return True
1184
- if os.path.isfile(os.path.join(project_root, 'pyproject.toml')):
1185
- return True
1186
- project_root = os.path.dirname(project_root)
1187
- return False
1188
-
1189
-
1190
- gsm_status = GsmStatus()
1191
- anki_results = {}
1192
- gsm_state = GsmAppState()
1193
- is_dev = is_running_from_source()
1194
-
1195
- is_beangate = os.path.exists("C:/Users/Beangate")
1196
-
1197
- logger.debug(f"Running in development mode: {is_dev}")
1198
- logger.debug(f"Running on Beangate's PC: {is_beangate}")