GameSentenceMiner 2.17.6__py3-none-any.whl → 2.18.0__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 (51) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +51 -51
  2. GameSentenceMiner/anki.py +236 -152
  3. GameSentenceMiner/gametext.py +7 -4
  4. GameSentenceMiner/gsm.py +49 -10
  5. GameSentenceMiner/locales/en_us.json +7 -3
  6. GameSentenceMiner/locales/ja_jp.json +8 -4
  7. GameSentenceMiner/locales/zh_cn.json +8 -4
  8. GameSentenceMiner/obs.py +238 -59
  9. GameSentenceMiner/ocr/owocr_helper.py +1 -1
  10. GameSentenceMiner/tools/ss_selector.py +7 -8
  11. GameSentenceMiner/ui/__init__.py +0 -0
  12. GameSentenceMiner/ui/anki_confirmation.py +187 -0
  13. GameSentenceMiner/{config_gui.py → ui/config_gui.py} +102 -37
  14. GameSentenceMiner/ui/screenshot_selector.py +215 -0
  15. GameSentenceMiner/util/configuration.py +124 -22
  16. GameSentenceMiner/util/db.py +22 -13
  17. GameSentenceMiner/util/downloader/download_tools.py +2 -2
  18. GameSentenceMiner/util/ffmpeg.py +24 -30
  19. GameSentenceMiner/util/get_overlay_coords.py +34 -34
  20. GameSentenceMiner/util/gsm_utils.py +31 -1
  21. GameSentenceMiner/util/text_log.py +11 -9
  22. GameSentenceMiner/vad.py +31 -12
  23. GameSentenceMiner/web/database_api.py +742 -123
  24. GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
  25. GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
  26. GameSentenceMiner/web/static/css/overview.css +850 -0
  27. GameSentenceMiner/web/static/css/popups-shared.css +126 -0
  28. GameSentenceMiner/web/static/css/shared.css +97 -0
  29. GameSentenceMiner/web/static/css/stats.css +192 -597
  30. GameSentenceMiner/web/static/js/anki_stats.js +6 -4
  31. GameSentenceMiner/web/static/js/database.js +209 -5
  32. GameSentenceMiner/web/static/js/goals.js +610 -0
  33. GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
  34. GameSentenceMiner/web/static/js/overview.js +1176 -0
  35. GameSentenceMiner/web/static/js/shared.js +25 -0
  36. GameSentenceMiner/web/static/js/stats.js +154 -1459
  37. GameSentenceMiner/web/stats.py +2 -2
  38. GameSentenceMiner/web/templates/anki_stats.html +5 -0
  39. GameSentenceMiner/web/templates/components/navigation.html +3 -1
  40. GameSentenceMiner/web/templates/database.html +73 -1
  41. GameSentenceMiner/web/templates/goals.html +376 -0
  42. GameSentenceMiner/web/templates/index.html +13 -11
  43. GameSentenceMiner/web/templates/overview.html +416 -0
  44. GameSentenceMiner/web/templates/stats.html +46 -251
  45. GameSentenceMiner/web/texthooking_page.py +18 -0
  46. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/METADATA +5 -1
  47. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/RECORD +51 -41
  48. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/WHEEL +0 -0
  49. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/entry_points.txt +0 -0
  50. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/licenses/LICENSE +0 -0
  51. {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/top_level.txt +0 -0
@@ -4,10 +4,13 @@ import logging
4
4
  import os
5
5
  import shutil
6
6
  import threading
7
+ import inspect
8
+
7
9
  from dataclasses import dataclass, field
8
10
  from logging.handlers import RotatingFileHandler
9
11
  from os.path import expanduser
10
12
  from sys import platform
13
+ import time
11
14
  from typing import List, Dict
12
15
  import sys
13
16
  from enum import Enum
@@ -19,6 +22,7 @@ from importlib import metadata
19
22
 
20
23
 
21
24
 
25
+
22
26
  OFF = 'OFF'
23
27
  # VOSK = 'VOSK'
24
28
  SILERO = 'SILERO'
@@ -355,7 +359,7 @@ def get_current_version():
355
359
  version = metadata.version(PACKAGE_NAME)
356
360
  return version
357
361
  except metadata.PackageNotFoundError:
358
- return None
362
+ return ""
359
363
 
360
364
 
361
365
  def get_latest_version():
@@ -429,6 +433,7 @@ class Paths:
429
433
  @dataclass
430
434
  class Anki:
431
435
  update_anki: bool = True
436
+ show_update_confirmation_dialog: bool = False
432
437
  url: str = 'http://127.0.0.1:8765'
433
438
  sentence_field: str = "Sentence"
434
439
  sentence_audio_field: str = "SentenceAudio"
@@ -464,6 +469,7 @@ class Features:
464
469
  open_anki_in_browser: bool = True
465
470
  browser_query: str = ''
466
471
  backfill_audio: bool = False
472
+ generate_longplay: bool = False
467
473
 
468
474
 
469
475
  @dataclass_json
@@ -529,12 +535,16 @@ class Audio:
529
535
  class OBS:
530
536
  open_obs: bool = True
531
537
  close_obs: bool = True
538
+ automatically_manage_replay_buffer: bool = True
532
539
  host: str = "127.0.0.1"
533
540
  port: int = 7274
534
541
  password: str = "your_password"
535
542
  get_game_from_scene: bool = True
536
543
  minimum_replay_size: int = 0
537
- turn_off_output_check: bool = False
544
+
545
+ def __post__init__(self):
546
+ # Force get_game_from_scene to be True
547
+ self.get_game_from_scene = True
538
548
 
539
549
 
540
550
  @dataclass_json
@@ -554,7 +564,7 @@ class VAD:
554
564
  language: str = 'ja'
555
565
  # vosk_url: str = VOSK_BASE
556
566
  selected_vad_model: str = WHISPER
557
- backup_vad_model: str = SILERO
567
+ backup_vad_model: str = OFF
558
568
  trim_beginning: bool = False
559
569
  beginning_offset: float = -0.25
560
570
  add_audio_on_no_results: bool = False
@@ -565,6 +575,10 @@ class VAD:
565
575
  use_cpu_for_inference: bool = False
566
576
  use_vad_filter_for_whisper: bool = True
567
577
 
578
+ def __post_init__(self):
579
+ if self.selected_vad_model == self.backup_vad_model:
580
+ self.backup_vad_model = OFF
581
+
568
582
  def is_silero(self):
569
583
  return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
570
584
 
@@ -797,6 +811,9 @@ class StatsConfig:
797
811
  reading_hours_target: int = 1500 # Target reading hours based on TMW N1 achievement data
798
812
  character_count_target: int = 25000000 # Target character count (25M) inspired by Discord server milestones
799
813
  games_target: int = 100 # Target VNs/games completed based on Refold community standards
814
+ reading_hours_target_date: str = "" # Target date for reading hours goal (ISO format: YYYY-MM-DD)
815
+ character_count_target_date: str = "" # Target date for character count goal (ISO format: YYYY-MM-DD)
816
+ games_target_date: str = "" # Target date for games/VNs goal (ISO format: YYYY-MM-DD)
800
817
 
801
818
  @dataclass_json
802
819
  @dataclass
@@ -806,6 +823,7 @@ class Config:
806
823
  switch_to_default_if_not_found: bool = True
807
824
  locale: str = Locale.English.value
808
825
  stats: StatsConfig = field(default_factory=StatsConfig)
826
+ version: str = ""
809
827
 
810
828
  @classmethod
811
829
  def new(cls):
@@ -842,6 +860,26 @@ class Config:
842
860
  self.stats.session_gap_seconds = profile.advanced.session_gap_seconds
843
861
  if profile.advanced.streak_requirement_hours != default_stats.streak_requirement_hours:
844
862
  self.stats.streak_requirement_hours = profile.advanced.streak_requirement_hours
863
+
864
+ # Add a way to migrate certain things based on version if needed, also help with better defaults
865
+ if self.version:
866
+ if self.version != get_current_version():
867
+ logger.info(f"Config version mismatch detected: {self.version} != {get_current_version()}")
868
+ # Handle version mismatch
869
+ changed = False
870
+ if self.version < "2.18.0":
871
+ changed = True
872
+ # Example, doesn't need to be done
873
+ for profile in self.configs.values():
874
+ profile.obs.get_game_from_scene = True
875
+ # Whisper basically uses Silero's VAD internally, so no need for backup
876
+ if profile.vad.selected_vad_model == WHISPER and profile.vad.backup_vad_model == SILERO:
877
+ profile.vad.backup_vad_model = OFF
878
+
879
+ if changed:
880
+ self.save()
881
+
882
+ self.version = get_current_version()
845
883
 
846
884
  def save(self):
847
885
  with open(get_config_path(), 'w') as file:
@@ -1000,8 +1038,40 @@ def get_app_directory():
1000
1038
  return config_dir
1001
1039
 
1002
1040
 
1041
+ def get_logger_name():
1042
+ """Determine the appropriate logger name based on the calling context."""
1043
+ frame = inspect.currentframe()
1044
+ try:
1045
+ # Go up the call stack to find the main module
1046
+ while frame:
1047
+ filename = frame.f_code.co_filename
1048
+ if filename.endswith(('gsm.py', 'gamesentenceminer.py', '__main__.py')):
1049
+ return "GameSentenceMiner"
1050
+ elif 'ocr' in filename.lower():
1051
+ return "misc_ocr_utils"
1052
+ elif 'overlay' in filename.lower():
1053
+ return "GSM_Overlay"
1054
+ frame = frame.f_back
1055
+
1056
+ # Fallback: check the main module name
1057
+ main_module = inspect.getmodule(inspect.stack()[-1][0])
1058
+ if main_module and hasattr(main_module, '__file__'):
1059
+ main_file = os.path.basename(main_module.__file__)
1060
+ if main_file in ('gsm.py', 'gamesentenceminer.py'):
1061
+ return "GameSentenceMiner"
1062
+ elif 'ocr' in main_file.lower():
1063
+ return "misc_ocr_utils"
1064
+ elif 'overlay' in main_file.lower():
1065
+ return "GSM_Overlay"
1066
+
1067
+ return "GameSentenceMiner" # Default fallback
1068
+ finally:
1069
+ del frame
1070
+
1071
+ logger_name = get_logger_name()
1072
+
1003
1073
  def get_log_path():
1004
- path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
1074
+ path = os.path.join(get_app_directory(), "logs", f'{logger_name.lower()}.log')
1005
1075
  os.makedirs(os.path.dirname(path), exist_ok=True)
1006
1076
  return path
1007
1077
 
@@ -1139,7 +1209,7 @@ def switch_profile_and_save(profile_name):
1139
1209
  sys.stdout.reconfigure(encoding='utf-8')
1140
1210
  sys.stderr.reconfigure(encoding='utf-8')
1141
1211
 
1142
- logger = logging.getLogger("GameSentenceMiner")
1212
+ logger = logging.getLogger(logger_name)
1143
1213
  # Set the base level to DEBUG so that all messages are captured
1144
1214
  logger.setLevel(logging.DEBUG)
1145
1215
  formatter = logging.Formatter(
@@ -1154,27 +1224,47 @@ console_handler.setFormatter(formatter)
1154
1224
  logger.addHandler(console_handler)
1155
1225
 
1156
1226
  file_path = get_log_path()
1157
- try:
1158
- if os.path.exists(file_path) and os.path.getsize(file_path) > 1 * 1024 * 1024 and os.access(file_path, os.W_OK):
1159
- old_log_path = os.path.join(os.path.dirname(
1160
- file_path), "gamesentenceminer_old.log")
1161
- if os.path.exists(old_log_path):
1162
- os.remove(old_log_path)
1163
- shutil.move(file_path, old_log_path)
1164
- except Exception as e:
1165
- logger.info(
1166
- "Couldn't rotate log, probably because the file is being written to by another process. NOT AN ERROR")
1167
-
1168
- file_handler = logging.FileHandler(file_path, encoding='utf-8')
1169
- file_handler.setLevel(logging.DEBUG)
1170
- file_handler.setFormatter(formatter)
1171
- logger.addHandler(file_handler)
1227
+ # Use RotatingFileHandler for automatic log rotation
1228
+ rotating_handler = RotatingFileHandler(
1229
+ file_path,
1230
+ maxBytes=10 * 1024 * 1024, # 10MB
1231
+ backupCount=5 if logger_name == "GameSentenceMiner" else 0, # Keep more logs for OCR and Overlay
1232
+ encoding='utf-8'
1233
+ )
1234
+ rotating_handler.setLevel(logging.DEBUG)
1235
+ rotating_handler.setFormatter(formatter)
1236
+ logger.addHandler(rotating_handler)
1172
1237
 
1173
1238
  DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
1174
1239
 
1175
1240
 
1241
+ # Clean up files in log directory older than 7 days
1242
+ def cleanup_old_logs(days=7):
1243
+ log_dir = os.path.dirname(get_log_path())
1244
+ now = time.time()
1245
+ cutoff = now - (days * 86400) # 86400 seconds in a day
1246
+
1247
+ if os.path.exists(log_dir):
1248
+ for filename in os.listdir(log_dir):
1249
+ file_path = os.path.join(log_dir, filename)
1250
+ if os.path.isfile(file_path):
1251
+ file_modified = os.path.getmtime(file_path)
1252
+ if file_modified < cutoff:
1253
+ try:
1254
+ os.remove(file_path)
1255
+ logger.info(f"Deleted old log file: {file_path}")
1256
+ except Exception as e:
1257
+ logger.error(f"Error deleting file {file_path}: {e}")
1258
+
1259
+ try:
1260
+ cleanup_old_logs()
1261
+ except Exception as e:
1262
+ logger.warning(f"Error during log cleanup: {e}")
1263
+
1264
+
1176
1265
  class GsmAppState:
1177
1266
  def __init__(self):
1267
+ self.config_app = None
1178
1268
  self.line_for_audio = None
1179
1269
  self.line_for_screenshot = None
1180
1270
  self.anki_note_for_screenshot = None
@@ -1184,11 +1274,15 @@ class GsmAppState:
1184
1274
  self.previous_audio = None
1185
1275
  self.previous_screenshot = None
1186
1276
  self.previous_replay = None
1277
+ self.current_replay = None
1187
1278
  self.lock = threading.Lock()
1188
1279
  self.last_mined_line = None
1189
1280
  self.keep_running = True
1190
1281
  self.current_game = ''
1191
1282
  self.videos_to_remove = set()
1283
+ self.recording_started_time = None
1284
+ self.current_srt = None
1285
+ self.srt_index = 1
1192
1286
 
1193
1287
 
1194
1288
  @dataclass_json
@@ -1237,8 +1331,6 @@ def is_running_from_source():
1237
1331
  while project_root != os.path.dirname(project_root): # Avoid infinite loop
1238
1332
  if os.path.isdir(os.path.join(project_root, '.git')):
1239
1333
  return True
1240
- if os.path.isfile(os.path.join(project_root, 'pyproject.toml')):
1241
- return True
1242
1334
  project_root = os.path.dirname(project_root)
1243
1335
  return False
1244
1336
 
@@ -1250,5 +1342,15 @@ is_dev = is_running_from_source()
1250
1342
 
1251
1343
  is_beangate = os.path.exists("C:/Users/Beangate")
1252
1344
 
1345
+
1346
+ def get_ffmpeg_path():
1347
+ return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
1348
+
1349
+ def get_ffprobe_path():
1350
+ return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
1351
+
1352
+ ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
1353
+
1354
+
1253
1355
  # logger.debug(f"Running in development mode: {is_dev}")
1254
1356
  # logger.debug(f"Running on Beangate's PC: {is_beangate}")
@@ -110,6 +110,11 @@ class SQLiteDBTable:
110
110
  pk_def = f"{cls._pk} TEXT PRIMARY KEY" if not cls._auto_increment else f"{cls._pk} INTEGER PRIMARY KEY AUTOINCREMENT"
111
111
  create_table_sql = f"CREATE TABLE IF NOT EXISTS {cls._table} ({pk_def}, {fields_def})"
112
112
  db.create_table(create_table_sql)
113
+ # Check for missing columns and add them
114
+ existing_columns = [col[1] for col in db.fetchall(f"PRAGMA table_info({cls._table})")]
115
+ for field in cls._fields:
116
+ if field not in existing_columns:
117
+ db.execute(f"ALTER TABLE {cls._table} ADD COLUMN {field} TEXT", commit=True)
113
118
 
114
119
  @classmethod
115
120
  def all(cls: Type[T]) -> List[T]:
@@ -135,15 +140,17 @@ class SQLiteDBTable:
135
140
  fields = [cls._pk] + cls._fields
136
141
  for i, field in enumerate(fields):
137
142
  if i == 0 and field == cls._pk:
138
- if cls._types[i] == int:
143
+ if cls._types[i] is int:
139
144
  setattr(obj, field, int(row[i])
140
145
  if row[i] is not None else None)
141
- elif cls._types[i] == str:
146
+ elif cls._types[i] is str:
142
147
  setattr(obj, field, str(row[i])
143
148
  if row[i] is not None else None)
144
149
  continue
145
- if cls._types[i] == str:
146
- if (row[i].startswith('[') or row[i].startswith('{')):
150
+ if cls._types[i] is str:
151
+ if not row[i]:
152
+ setattr(obj, field, "")
153
+ elif (row[i].startswith('[') or row[i].startswith('{')):
147
154
  try:
148
155
  setattr(obj, field, json.loads(row[i]))
149
156
  except json.JSONDecodeError:
@@ -151,21 +158,21 @@ class SQLiteDBTable:
151
158
  else:
152
159
  setattr(obj, field, str(row[i])
153
160
  if row[i] is not None else None)
154
- elif cls._types[i] == list:
161
+ elif cls._types[i] is list:
155
162
  try:
156
163
  setattr(obj, field, json.loads(row[i]) if row[i] else [])
157
164
  except json.JSONDecodeError:
158
165
  setattr(obj, field, [])
159
- elif cls._types[i] == int:
166
+ elif cls._types[i] is int:
160
167
  setattr(obj, field, int(row[i])
161
168
  if row[i] is not None else None)
162
- elif cls._types[i] == float:
169
+ elif cls._types[i] is float:
163
170
  setattr(obj, field, float(row[i])
164
171
  if row[i] is not None else None)
165
- elif cls._types[i] == bool:
172
+ elif cls._types[i] is bool:
166
173
  setattr(obj, field, bool(row[i])
167
174
  if row[i] is not None else None)
168
- elif cls._types[i] == dict:
175
+ elif cls._types[i] is dict:
169
176
  try:
170
177
  setattr(obj, field, json.loads(row[i]) if row[i] else {})
171
178
  except json.JSONDecodeError:
@@ -212,7 +219,7 @@ class SQLiteDBTable:
212
219
  def add(self, retry=1):
213
220
  try:
214
221
  pk_val = getattr(self, self._pk, None)
215
- if cls._auto_increment:
222
+ if self._auto_increment:
216
223
  self.save()
217
224
  elif pk_val is None:
218
225
  raise ValueError(
@@ -378,9 +385,9 @@ class AIModelsTable(SQLiteDBTable):
378
385
  class GameLinesTable(SQLiteDBTable):
379
386
  _table = 'game_lines'
380
387
  _fields = ['game_name', 'line_text', 'screenshot_in_anki',
381
- 'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation', 'timestamp']
388
+ 'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation', 'timestamp', 'original_game_name']
382
389
  _types = [str, # Includes primary key type
383
- str, str, str, str, str, str, str, str, float]
390
+ str, str, str, str, str, str, str, str, float, str]
384
391
  _pk = 'id'
385
392
  _auto_increment = False # Use string IDs
386
393
 
@@ -394,7 +401,8 @@ class GameLinesTable(SQLiteDBTable):
394
401
  screenshot_path: Optional[str] = None,
395
402
  audio_path: Optional[str] = None,
396
403
  replay_path: Optional[str] = None,
397
- translation: Optional[str] = None):
404
+ translation: Optional[str] = None,
405
+ original_game_name: Optional[str] = None):
398
406
  self.id = id
399
407
  self.game_name = game_name
400
408
  self.line_text = line_text
@@ -406,6 +414,7 @@ class GameLinesTable(SQLiteDBTable):
406
414
  self.audio_path = audio_path if audio_path is not None else ''
407
415
  self.replay_path = replay_path if replay_path is not None else ''
408
416
  self.translation = translation if translation is not None else ''
417
+ self.original_game_name = original_game_name if original_game_name is not None else ''
409
418
 
410
419
  @classmethod
411
420
  def get_all_lines_for_scene(cls, game_name: str) -> List['GameLinesTable']:
@@ -7,8 +7,8 @@ import platform
7
7
  import zipfile
8
8
 
9
9
  from GameSentenceMiner.util.downloader.Untitled_json import scenes
10
- from GameSentenceMiner.util.configuration import get_app_directory, logger
11
- from GameSentenceMiner.util.ffmpeg import get_ffmpeg_path, get_ffprobe_path
10
+ from GameSentenceMiner.util.configuration import get_app_directory, get_ffmpeg_path, logger
11
+ from GameSentenceMiner.util.configuration import get_ffprobe_path
12
12
  from GameSentenceMiner.obs import get_obs_path
13
13
  import tempfile
14
14
 
@@ -5,23 +5,20 @@ import sys
5
5
  import tempfile
6
6
  import time
7
7
  from pathlib import Path
8
+ import subprocess
9
+ from pathlib import Path
10
+ import shutil
11
+
8
12
 
9
13
  from GameSentenceMiner import obs
10
- from GameSentenceMiner.util.configuration import get_app_directory, is_windows, logger, get_config, \
14
+ from GameSentenceMiner.ui.config_gui import ConfigApp
15
+ from GameSentenceMiner.util.configuration import ffmpeg_base_command_list, get_ffprobe_path, logger, get_config, \
11
16
  get_temporary_directory, gsm_state, is_linux
12
17
  from GameSentenceMiner.util.gsm_utils import make_unique_file_name, get_file_modification_time
13
18
  from GameSentenceMiner.util import configuration
14
19
  from GameSentenceMiner.util.text_log import initial_time
15
20
 
16
21
 
17
- def get_ffmpeg_path():
18
- return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
19
-
20
- def get_ffprobe_path():
21
- return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
22
-
23
- ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
24
-
25
22
  supported_formats = {
26
23
  'opus': 'libopus',
27
24
  'mp3': 'libmp3lame',
@@ -30,11 +27,6 @@ supported_formats = {
30
27
  'm4a': 'aac',
31
28
  }
32
29
 
33
- import subprocess
34
- from pathlib import Path
35
- import shutil
36
-
37
-
38
30
  def video_to_anim(
39
31
  input_path: str | Path,
40
32
  output_path: str | Path = None,
@@ -184,22 +176,24 @@ def call_frame_extractor(video_path, timestamp):
184
176
  str: The path of the selected image, or None on error.
185
177
  """
186
178
  try:
187
- logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp)]))
188
-
189
- # Run the script using subprocess.run()
190
- result = subprocess.run(
191
- [sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
192
- capture_output=True,
193
- text=True, # Get output as text
194
- check=False # Raise an exception for non-zero exit codes
195
- )
196
- if result.returncode != 0:
197
- logger.error(f"Script failed with return code: {result.returncode}")
198
- return None
199
- logger.info(result)
200
- # Print the standard output
201
- logger.info(f"Frame extractor script output: {result.stdout.strip()}")
202
- return result.stdout.strip() # Return the output
179
+ config_app: ConfigApp = gsm_state.config_app
180
+ return config_app.show_screenshot_selector(video_path, timestamp, get_config().screenshot.screenshot_timing_setting)
181
+ # logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp)]))
182
+
183
+ # # Run the script using subprocess.run()
184
+ # result = subprocess.run(
185
+ # [sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
186
+ # capture_output=True,
187
+ # text=True, # Get output as text
188
+ # check=False # Raise an exception for non-zero exit codes
189
+ # )
190
+ # if result.returncode != 0:
191
+ # logger.error(f"Script failed with return code: {result.returncode}")
192
+ # return None
193
+ # logger.info(result)
194
+ # # Print the standard output
195
+ # logger.info(f"Frame extractor script output: {result.stdout.strip()}")
196
+ # return result.stdout.strip() # Return the output
203
197
 
204
198
  except subprocess.CalledProcessError as e:
205
199
  logger.error(f"Error calling script: {e}")
@@ -190,37 +190,38 @@ class OverlayProcessor:
190
190
  """
191
191
  with mss.mss() as sct:
192
192
  monitors = sct.monitors[1:]
193
- if is_windows() and monitor_index == 0:
194
- from ctypes import wintypes
195
- import ctypes
196
- # Get work area for primary monitor (ignores taskbar)
197
- SPI_GETWORKAREA = 0x0030
198
- rect = wintypes.RECT()
199
- res = ctypes.windll.user32.SystemParametersInfoW(
200
- SPI_GETWORKAREA, 0, ctypes.byref(rect), 0
201
- )
202
- if not res:
203
- raise ctypes.WinError()
193
+ return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
194
+ # if is_windows() and monitor_index == 0:
195
+ # from ctypes import wintypes
196
+ # import ctypes
197
+ # # Get work area for primary monitor (ignores taskbar)
198
+ # SPI_GETWORKAREA = 0x0030
199
+ # rect = wintypes.RECT()
200
+ # res = ctypes.windll.user32.SystemParametersInfoW(
201
+ # SPI_GETWORKAREA, 0, ctypes.byref(rect), 0
202
+ # )
203
+ # if not res:
204
+ # raise ctypes.WinError()
204
205
 
205
- return {
206
- "left": rect.left,
207
- "top": rect.top,
208
- "width": rect.right - rect.left,
209
- "height": rect.bottom - rect.top,
210
- }
211
- elif is_windows() and monitor_index > 0:
212
- # Secondary monitors: just return with a guess of how tall the taskbar is
213
- taskbar_height_guess = 48 # A common taskbar height, may vary
214
- mon = monitors[monitor_index]
215
- return {
216
- "left": mon["left"],
217
- "top": mon["top"],
218
- "width": mon["width"],
219
- "height": mon["height"] - taskbar_height_guess
220
- }
221
- else:
222
- # For non-Windows systems or unspecified monitors, return the monitor area as-is
223
- return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
206
+ # return {
207
+ # "left": rect.left,
208
+ # "top": rect.top,
209
+ # "width": rect.right - rect.left,
210
+ # "height": rect.bottom - rect.top,
211
+ # }
212
+ # elif is_windows() and monitor_index > 0:
213
+ # # Secondary monitors: just return with a guess of how tall the taskbar is
214
+ # taskbar_height_guess = 48 # A common taskbar height, may vary
215
+ # mon = monitors[monitor_index]
216
+ # return {
217
+ # "left": mon["left"],
218
+ # "top": mon["top"],
219
+ # "width": mon["width"],
220
+ # "height": mon["height"] - taskbar_height_guess
221
+ # }
222
+ # else:
223
+ # # For non-Windows systems or unspecified monitors, return the monitor area as-is
224
+ # return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
224
225
 
225
226
 
226
227
  def _get_full_screenshot(self) -> Tuple[Image.Image | None, int, int]:
@@ -309,11 +310,9 @@ class OverlayProcessor:
309
310
 
310
311
  score = fuzz.ratio(text_str, self.last_oneocr_result)
311
312
  if score >= 80:
312
- logger.info("OneOCR results are similar to the last results (score: %d). Skipping overlay update.", score)
313
313
  return
314
314
  self.last_oneocr_result = text_str
315
315
 
316
- logger.info("Sending OneOCR results to overlay.")
317
316
  await send_word_coordinates_to_overlay(self._convert_oneocr_results_to_percentages(oneocr_results, monitor_width, monitor_height))
318
317
 
319
318
  # If User Home is beangate
@@ -322,7 +321,7 @@ class OverlayProcessor:
322
321
  f.write(json.dumps(oneocr_results, ensure_ascii=False, indent=2))
323
322
 
324
323
  if get_config().overlay.engine == OverlayEngine.ONEOCR.value and self.oneocr:
325
- logger.info("Using OneOCR results for overlay as configured.")
324
+ logger.info("Sent %d text boxes to overlay.", len(oneocr_results))
326
325
  return
327
326
 
328
327
  # 3. Create a composite image with only the detected text regions
@@ -371,8 +370,9 @@ class OverlayProcessor:
371
370
  crop_height=composite_image.height,
372
371
  use_percentages=True
373
372
  )
374
- logger.info("Sending Google Lens results to overlay.")
375
373
  await send_word_coordinates_to_overlay(extracted_data)
374
+
375
+ logger.info("Sent %d text boxes to overlay.", len(extracted_data))
376
376
 
377
377
  def _extract_text_with_pixel_boxes(
378
378
  self,
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
  import requests
14
14
  from rapidfuzz import process
15
15
 
16
- from GameSentenceMiner.util.configuration import logger, get_config, get_app_directory
16
+ from GameSentenceMiner.util.configuration import gsm_state, logger, get_config, get_app_directory, get_temporary_directory
17
17
 
18
18
  SCRIPTS_DIR = r"E:\Japanese Stuff\agent-v0.1.4-win32-x64\data\scripts"
19
19
 
@@ -22,6 +22,13 @@ def run_new_thread(func):
22
22
  thread.start()
23
23
  return thread
24
24
 
25
+ def make_unique_temp_file(path):
26
+ path = Path(path)
27
+ current_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%f')[:-3]
28
+ temp_dir = get_temporary_directory()
29
+ os.makedirs(temp_dir, exist_ok=True)
30
+ return str(Path(temp_dir) / f"{path.stem}_{current_time}{path.suffix}")
31
+
25
32
  def make_unique_file_name(path):
26
33
  path = Path(path)
27
34
  current_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%f')[:-3]
@@ -258,6 +265,29 @@ TEXT_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'text_repla
258
265
  OCR_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'ocr_replacements.json')
259
266
  os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
260
267
 
268
+
269
+ def add_srt_line(line_time, new_line):
270
+ global srt_index
271
+ if get_config().features.generate_longplay and gsm_state.recording_started_time and new_line.prev:
272
+ logger.info(f"Adding SRT line {new_line.prev.text}... for longplay")
273
+ with open(gsm_state.current_srt, 'a', encoding='utf-8') as srt_file:
274
+ # Calculate start and end times for the previous line
275
+ prev_start_time = new_line.prev.time - gsm_state.recording_started_time
276
+ prev_end_time = (line_time if line_time else datetime.now()) - gsm_state.recording_started_time
277
+ # Format times as SRT timestamps (HH:MM:SS,mmm)
278
+ def format_srt_time(td, offset=0):
279
+ total_seconds = int(td.total_seconds()) + offset
280
+ hours = total_seconds // 3600
281
+ minutes = (total_seconds % 3600) // 60
282
+ seconds = total_seconds % 60
283
+ milliseconds = int(td.microseconds / 1000)
284
+ return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
285
+
286
+ srt_file.write(f"{gsm_state.srt_index}\n")
287
+ srt_file.write(f"{format_srt_time(prev_start_time)} --> {format_srt_time(prev_end_time, offset=-1)}\n")
288
+ srt_file.write(f"{new_line.prev.text}\n\n")
289
+ gsm_state.srt_index += 1
290
+
261
291
  # if not os.path.exists(OCR_REPLACEMENTS_FILE):
262
292
  # url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/refs/heads/main/electron-src/assets/ocr_replacements.json"
263
293
  # try: