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.
- GameSentenceMiner/ai/ai_prompting.py +51 -51
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +102 -37
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/METADATA +5 -1
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/RECORD +51 -41
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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 =
|
|
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", '
|
|
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(
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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}")
|
GameSentenceMiner/util/db.py
CHANGED
|
@@ -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]
|
|
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]
|
|
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]
|
|
146
|
-
if
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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.
|
|
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
|
|
GameSentenceMiner/util/ffmpeg.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
logger.
|
|
200
|
-
#
|
|
201
|
-
logger.info(
|
|
202
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
elif is_windows() and monitor_index > 0:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
else:
|
|
222
|
-
|
|
223
|
-
|
|
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("
|
|
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:
|