GameSentenceMiner 2.17.0__py3-none-any.whl → 2.17.2__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.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/anki.py +31 -3
- GameSentenceMiner/config_gui.py +26 -2
- GameSentenceMiner/gametext.py +4 -3
- GameSentenceMiner/gsm.py +19 -23
- GameSentenceMiner/obs.py +17 -7
- GameSentenceMiner/ocr/owocr_helper.py +11 -8
- GameSentenceMiner/owocr/owocr/run.py +11 -5
- GameSentenceMiner/util/configuration.py +7 -5
- GameSentenceMiner/util/db.py +176 -8
- GameSentenceMiner/util/downloader/download_tools.py +57 -24
- GameSentenceMiner/util/ffmpeg.py +5 -2
- GameSentenceMiner/util/get_overlay_coords.py +3 -0
- GameSentenceMiner/util/gsm_utils.py +0 -54
- GameSentenceMiner/vad.py +5 -2
- GameSentenceMiner/web/database_api.py +12 -1
- GameSentenceMiner/web/gsm_websocket.py +1 -1
- GameSentenceMiner/web/static/css/shared.css +20 -0
- GameSentenceMiner/web/static/css/stats.css +496 -1
- GameSentenceMiner/web/static/js/anki_stats.js +87 -3
- GameSentenceMiner/web/static/js/shared.js +2 -49
- GameSentenceMiner/web/static/js/stats.js +274 -39
- GameSentenceMiner/web/templates/anki_stats.html +36 -0
- GameSentenceMiner/web/templates/index.html +1 -1
- GameSentenceMiner/web/templates/stats.html +35 -15
- GameSentenceMiner/web/texthooking_page.py +31 -8
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/RECORD +31 -31
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/top_level.txt +0 -0
GameSentenceMiner/util/db.py
CHANGED
|
@@ -264,6 +264,50 @@ class SQLiteDBTable:
|
|
|
264
264
|
@classmethod
|
|
265
265
|
def drop(cls):
|
|
266
266
|
cls._db.execute(f"DROP TABLE IF EXISTS {cls._table}", commit=True)
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def has_column(cls, column_name: str) -> bool:
|
|
270
|
+
row = cls._db.fetchone(
|
|
271
|
+
f"PRAGMA table_info({cls._table})")
|
|
272
|
+
if not row:
|
|
273
|
+
return False
|
|
274
|
+
columns = [col[1] for col in cls._db.fetchall(
|
|
275
|
+
f"PRAGMA table_info({cls._table})")]
|
|
276
|
+
return column_name in columns
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def rename_column(cls, old_column: str, new_column: str):
|
|
280
|
+
cls._db.execute(
|
|
281
|
+
f"ALTER TABLE {cls._table} RENAME COLUMN {old_column} TO {new_column}", commit=True)
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def drop_column(cls, column_name: str):
|
|
285
|
+
cls._db.execute(
|
|
286
|
+
f"ALTER TABLE {cls._table} DROP COLUMN {column_name}", commit=True)
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def get_column_type(cls, column_name: str) -> Optional[str]:
|
|
290
|
+
row = cls._db.fetchone(
|
|
291
|
+
f"PRAGMA table_info({cls._table})")
|
|
292
|
+
if not row:
|
|
293
|
+
return None
|
|
294
|
+
columns = cls._db.fetchall(
|
|
295
|
+
f"PRAGMA table_info({cls._table})")
|
|
296
|
+
for col in columns:
|
|
297
|
+
if col[1] == column_name:
|
|
298
|
+
return col[2] # Return the type
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def alter_column_type(cls, old_column: str, new_column: str, new_type: str):
|
|
303
|
+
# Add new column
|
|
304
|
+
cls._db.execute(
|
|
305
|
+
f"ALTER TABLE {cls._table} ADD COLUMN {new_column} {new_type}", commit=True)
|
|
306
|
+
# Copy and cast data
|
|
307
|
+
cls._db.execute(
|
|
308
|
+
f"UPDATE {cls._table} SET {new_column} = CAST({old_column} AS {new_type})", commit=True)
|
|
309
|
+
cls._db.execute(
|
|
310
|
+
f"ALTER TABLE {cls._table} DROP COLUMN {old_column}", commit=True)
|
|
267
311
|
|
|
268
312
|
|
|
269
313
|
class AIModelsTable(SQLiteDBTable):
|
|
@@ -333,10 +377,10 @@ class AIModelsTable(SQLiteDBTable):
|
|
|
333
377
|
|
|
334
378
|
class GameLinesTable(SQLiteDBTable):
|
|
335
379
|
_table = 'game_lines'
|
|
336
|
-
_fields = ['game_name', 'line_text', '
|
|
337
|
-
'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation']
|
|
380
|
+
_fields = ['game_name', 'line_text', 'screenshot_in_anki',
|
|
381
|
+
'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation', 'timestamp']
|
|
338
382
|
_types = [str, # Includes primary key type
|
|
339
|
-
str, str, str, str, str, str, str, str,
|
|
383
|
+
str, str, str, str, str, str, str, str, float]
|
|
340
384
|
_pk = 'id'
|
|
341
385
|
_auto_increment = False # Use string IDs
|
|
342
386
|
|
|
@@ -355,7 +399,7 @@ class GameLinesTable(SQLiteDBTable):
|
|
|
355
399
|
self.game_name = game_name
|
|
356
400
|
self.line_text = line_text
|
|
357
401
|
self.context = context
|
|
358
|
-
self.timestamp = timestamp if timestamp is not None else datetime.now().timestamp()
|
|
402
|
+
self.timestamp = float(timestamp) if timestamp is not None else datetime.now().timestamp()
|
|
359
403
|
self.screenshot_in_anki = screenshot_in_anki if screenshot_in_anki is not None else ''
|
|
360
404
|
self.audio_in_anki = audio_in_anki if audio_in_anki is not None else ''
|
|
361
405
|
self.screenshot_path = screenshot_path if screenshot_path is not None else ''
|
|
@@ -416,9 +460,79 @@ class GameLinesTable(SQLiteDBTable):
|
|
|
416
460
|
params,
|
|
417
461
|
commit=True
|
|
418
462
|
)
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def get_lines_filtered_by_timestamp(cls, start: Optional[float] = None, end: Optional[float] = None) -> List['GameLinesTable']:
|
|
466
|
+
"""
|
|
467
|
+
Fetches all lines optionally filtered by start and end timestamps.
|
|
468
|
+
If start or end is None, that bound is ignored.
|
|
469
|
+
"""
|
|
470
|
+
query = f"SELECT * FROM {cls._table}"
|
|
471
|
+
conditions = []
|
|
472
|
+
params = []
|
|
473
|
+
|
|
474
|
+
# Add timestamp conditions if provided
|
|
475
|
+
if start is not None:
|
|
476
|
+
conditions.append("timestamp >= ?")
|
|
477
|
+
params.append(start)
|
|
478
|
+
if end is not None:
|
|
479
|
+
conditions.append("timestamp <= ?")
|
|
480
|
+
params.append(end)
|
|
481
|
+
|
|
482
|
+
# Combine conditions into WHERE clause if any
|
|
483
|
+
if conditions:
|
|
484
|
+
query += " WHERE " + " AND ".join(conditions)
|
|
485
|
+
|
|
486
|
+
# Sort by timestamp ascending
|
|
487
|
+
query += " ORDER BY timestamp ASC"
|
|
488
|
+
|
|
489
|
+
# Execute the query
|
|
490
|
+
rows = cls._db.fetchall(query, tuple(params))
|
|
491
|
+
return [cls.from_row(row) for row in rows]
|
|
492
|
+
|
|
493
|
+
class StatsRollupTable(SQLiteDBTable):
|
|
494
|
+
_table = 'stats_rollup'
|
|
495
|
+
_fields = ['date', 'games_played', 'lines_mined', 'anki_cards_created', 'time_spent_mining']
|
|
496
|
+
_types = [int, # Includes primary key type
|
|
497
|
+
str, int, int, int, float]
|
|
498
|
+
_pk = 'id'
|
|
499
|
+
_auto_increment = True # Use auto-incrementing integer IDs
|
|
500
|
+
|
|
501
|
+
def __init__(self, id: Optional[int] = None,
|
|
502
|
+
date: Optional[str] = None,
|
|
503
|
+
games_played: int = 0,
|
|
504
|
+
lines_mined: int = 0,
|
|
505
|
+
anki_cards_created: int = 0,
|
|
506
|
+
time_spent_mining: float = 0.0):
|
|
507
|
+
self.id = id
|
|
508
|
+
self.date = date if date is not None else datetime.now().strftime("%Y-%m-%d")
|
|
509
|
+
self.games_played = games_played
|
|
510
|
+
self.lines_mined = lines_mined
|
|
511
|
+
self.anki_cards_created = anki_cards_created
|
|
512
|
+
self.time_spent_mining = time_spent_mining
|
|
419
513
|
|
|
514
|
+
@classmethod
|
|
515
|
+
def get_stats_for_date(cls, date: str) -> Optional['StatsRollupTable']:
|
|
516
|
+
row = cls._db.fetchone(
|
|
517
|
+
f"SELECT * FROM {cls._table} WHERE date=?", (date,))
|
|
518
|
+
return cls.from_row(row) if row else None
|
|
420
519
|
|
|
421
|
-
|
|
520
|
+
@classmethod
|
|
521
|
+
def update_stats(cls, date: str, games_played: int = 0, lines_mined: int = 0, anki_cards_created: int = 0, time_spent_mining: float = 0.0):
|
|
522
|
+
stats = cls.get_stats_for_date(date)
|
|
523
|
+
if not stats:
|
|
524
|
+
new_stats = cls(date=date, games_played=games_played,
|
|
525
|
+
lines_mined=lines_mined, anki_cards_created=anki_cards_created, time_spent_mining=time_spent_mining)
|
|
526
|
+
new_stats.save()
|
|
527
|
+
return
|
|
528
|
+
stats.games_played += games_played
|
|
529
|
+
stats.lines_mined += lines_mined
|
|
530
|
+
stats.anki_cards_created += anki_cards_created
|
|
531
|
+
stats.time_spent_mining += time_spent_mining
|
|
532
|
+
stats.save()
|
|
533
|
+
|
|
534
|
+
# Ensure database directory exists and return path
|
|
535
|
+
def get_db_directory(test=False, delete_test=False) -> str:
|
|
422
536
|
if platform == 'win32': # Windows
|
|
423
537
|
appdata_dir = os.getenv('APPDATA')
|
|
424
538
|
else: # macOS and Linux
|
|
@@ -426,7 +540,11 @@ def get_db_directory():
|
|
|
426
540
|
config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
|
|
427
541
|
# Create the directory if it doesn't exist
|
|
428
542
|
os.makedirs(config_dir, exist_ok=True)
|
|
429
|
-
|
|
543
|
+
path = os.path.join(config_dir, 'gsm.db' if not test else 'gsm_test.db')
|
|
544
|
+
if test and delete_test:
|
|
545
|
+
if os.path.exists(path):
|
|
546
|
+
os.remove(path)
|
|
547
|
+
return path
|
|
430
548
|
|
|
431
549
|
|
|
432
550
|
# Backup and compress the database on load, with today's date, up to 5 days ago (clean up old backups)
|
|
@@ -462,6 +580,8 @@ db_path = get_db_directory()
|
|
|
462
580
|
if os.path.exists(db_path):
|
|
463
581
|
backup_db(db_path)
|
|
464
582
|
|
|
583
|
+
# db_path = get_db_directory(test=True, delete_test=False)
|
|
584
|
+
|
|
465
585
|
gsm_db = SQLiteDB(db_path)
|
|
466
586
|
|
|
467
587
|
for cls in [AIModelsTable, GameLinesTable]:
|
|
@@ -470,13 +590,47 @@ for cls in [AIModelsTable, GameLinesTable]:
|
|
|
470
590
|
# cls.drop()
|
|
471
591
|
# cls.set_db(gsm_db) # --- IGNORE ---
|
|
472
592
|
|
|
593
|
+
# GameLinesTable.drop_column('timestamp')
|
|
594
|
+
|
|
595
|
+
# if GameLinesTable.has_column('timestamp_old'):
|
|
596
|
+
# GameLinesTable.alter_column_type('timestamp_old', 'timestamp', 'TEXT')
|
|
597
|
+
# logger.info("Altered 'timestamp_old' column to 'timestamp' with TEXT type in GameLinesTable.")
|
|
598
|
+
|
|
599
|
+
def check_and_run_migrations():
|
|
600
|
+
def migrate_timestamp():
|
|
601
|
+
if GameLinesTable.has_column('timestamp') and GameLinesTable.get_column_type('timestamp') != 'REAL':
|
|
602
|
+
logger.info("Migrating 'timestamp' column to REAL type in GameLinesTable.")
|
|
603
|
+
# Rename 'timestamp' to 'timestamp_old'
|
|
604
|
+
GameLinesTable.rename_column('timestamp', 'timestamp_old')
|
|
605
|
+
# Copy and cast data from old column to new column
|
|
606
|
+
GameLinesTable.alter_column_type('timestamp_old', 'timestamp', 'REAL')
|
|
607
|
+
logger.info("Migrated 'timestamp' column to REAL type in GameLinesTable.")
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
migrate_timestamp()
|
|
611
|
+
|
|
612
|
+
check_and_run_migrations()
|
|
613
|
+
|
|
614
|
+
# all_lines = GameLinesTable.all()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# # Convert String timestamp to float timestamp
|
|
618
|
+
# for line in all_lines:
|
|
619
|
+
# if isinstance(line.timestamp, str):
|
|
620
|
+
# try:
|
|
621
|
+
# line.timestamp = float(line.timestamp)
|
|
622
|
+
# except ValueError:
|
|
623
|
+
# # Handle invalid timestamp format
|
|
624
|
+
# line.timestamp = 0.0
|
|
625
|
+
# line.save()
|
|
626
|
+
|
|
473
627
|
# import random
|
|
474
628
|
# import uuid
|
|
475
629
|
# from datetime import datetime
|
|
476
630
|
# from GameSentenceMiner.util.text_log import GameLine
|
|
477
631
|
# from GameSentenceMiner.util.db import GameLinesTable
|
|
478
632
|
|
|
479
|
-
# List of common Japanese characters (kanji, hiragana, katakana)
|
|
633
|
+
# # List of common Japanese characters (kanji, hiragana, katakana)
|
|
480
634
|
# japanese_chars = (
|
|
481
635
|
# "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん"
|
|
482
636
|
# "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
|
|
@@ -513,6 +667,7 @@ for cls in [AIModelsTable, GameLinesTable]:
|
|
|
513
667
|
|
|
514
668
|
# if len(lines_batch) >= batch_size:
|
|
515
669
|
# GameLinesTable.add_lines(lines_batch)
|
|
670
|
+
# GameLinesTable2.add_lines(lines_batch)
|
|
516
671
|
# lines_batch = []
|
|
517
672
|
# if i % 1000 == 0:
|
|
518
673
|
# print(f"Inserted {i} lines...")
|
|
@@ -520,5 +675,18 @@ for cls in [AIModelsTable, GameLinesTable]:
|
|
|
520
675
|
# # Insert any remaining lines
|
|
521
676
|
# if lines_batch:
|
|
522
677
|
# GameLinesTable.add_lines(lines_batch)
|
|
678
|
+
# GameLinesTable2.add_lines(lines_batch)
|
|
679
|
+
# for _ in range(10): # Run multiple times to see consistent timing
|
|
680
|
+
# start_time = time.time()
|
|
681
|
+
# GameLinesTable.all()
|
|
682
|
+
# end_time = time.time()
|
|
683
|
+
|
|
684
|
+
# print(f"Time taken to query all lines from GameLinesTable: {end_time - start_time:.2f} seconds")
|
|
685
|
+
|
|
686
|
+
# start_time = time.time()
|
|
687
|
+
# GameLinesTable2.all()
|
|
688
|
+
# end_time = time.time()
|
|
689
|
+
|
|
690
|
+
# print(f"Time taken to query all lines from GameLinesTable2: {end_time - start_time:.2f} seconds")
|
|
523
691
|
|
|
524
|
-
print("Done populating
|
|
692
|
+
# print("Done populating GameLinesTable and GameLinesTable2 with random Japanese text.")
|
|
@@ -71,6 +71,7 @@ def download_obs_if_needed():
|
|
|
71
71
|
def get_windows_obs_url():
|
|
72
72
|
machine = platform.machine().lower()
|
|
73
73
|
if machine in ['arm64', 'aarch64']:
|
|
74
|
+
logger.info("Detected Windows on ARM64. Getting ARM64 version of OBS Studio.")
|
|
74
75
|
return next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
75
76
|
asset['name'].endswith('Windows-arm64.zip'))
|
|
76
77
|
return next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
@@ -80,12 +81,12 @@ def download_obs_if_needed():
|
|
|
80
81
|
with urllib.request.urlopen(latest_release_url) as response:
|
|
81
82
|
latest_release = json.load(response)
|
|
82
83
|
obs_url = {
|
|
83
|
-
"Windows": get_windows_obs_url
|
|
84
|
-
"Linux": next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
85
|
-
|
|
86
|
-
"Darwin": next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
87
|
-
|
|
88
|
-
}.get(platform.system(), None)
|
|
84
|
+
"Windows": get_windows_obs_url,
|
|
85
|
+
# "Linux": lambda: next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
86
|
+
# asset['name'].endswith('Ubuntu-24.04-x86_64.deb')),
|
|
87
|
+
# "Darwin": lambda: next(asset['browser_download_url'] for asset in latest_release['assets'] if
|
|
88
|
+
# asset['name'].endswith('macOS-Intel.dmg'))
|
|
89
|
+
}.get(platform.system(), lambda: None)()
|
|
89
90
|
|
|
90
91
|
if obs_url is None:
|
|
91
92
|
logger.error("Unsupported OS. Please install OBS manually.")
|
|
@@ -163,43 +164,75 @@ def download_ffmpeg_if_needed():
|
|
|
163
164
|
logger.info("FFmpeg directory exists but executables are missing. Re-downloading FFmpeg...")
|
|
164
165
|
shutil.rmtree(ffmpeg_dir)
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
system = platform.system()
|
|
168
|
+
ffmpeg_url = None
|
|
169
|
+
compressed_format = "zip"
|
|
170
|
+
if system == "Windows":
|
|
171
|
+
machine = platform.machine().lower()
|
|
172
|
+
if machine in ['arm64', 'aarch64']:
|
|
173
|
+
ffmpeg_url = "https://gsm.beangate.us/ffmpeg-8.0-essentials-shared-win-arm64.zip"
|
|
174
|
+
compressed_format = "zip"
|
|
175
|
+
else:
|
|
176
|
+
ffmpeg_url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
|
177
|
+
compressed_format = "zip"
|
|
178
|
+
# elif system == "Linux":
|
|
179
|
+
# ffmpeg_url = "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
|
|
180
|
+
# elif system == "Darwin":
|
|
181
|
+
# ffmpeg_url = "https://evermeet.cx/ffmpeg/ffmpeg.zip"
|
|
171
182
|
|
|
172
183
|
if ffmpeg_url is None:
|
|
173
|
-
logger.error("Unsupported OS. Please install FFmpeg manually.")
|
|
184
|
+
logger.error("Unsupported OS/architecture. Please install FFmpeg manually.")
|
|
174
185
|
return
|
|
175
186
|
|
|
176
187
|
download_dir = os.path.join(get_app_directory(), "downloads")
|
|
177
188
|
os.makedirs(download_dir, exist_ok=True)
|
|
178
|
-
ffmpeg_archive = os.path.join(download_dir, "ffmpeg.
|
|
189
|
+
ffmpeg_archive = os.path.join(download_dir, f"ffmpeg.{compressed_format}")
|
|
179
190
|
|
|
180
191
|
logger.info(f"Downloading FFmpeg from {ffmpeg_url}...")
|
|
181
192
|
urllib.request.urlretrieve(ffmpeg_url, ffmpeg_archive)
|
|
182
193
|
logger.info(f"FFmpeg downloaded. Extracting to {ffmpeg_dir}...")
|
|
183
194
|
|
|
184
195
|
os.makedirs(ffmpeg_dir, exist_ok=True)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
|
|
197
|
+
# Extract 7z
|
|
198
|
+
# Extract archive
|
|
199
|
+
if ffmpeg_url.endswith('.7z'):
|
|
200
|
+
with py7zr.SevenZipFile(ffmpeg_archive, mode='r') as z:
|
|
201
|
+
z.extractall(ffmpeg_dir)
|
|
202
|
+
else:
|
|
203
|
+
with zipfile.ZipFile(ffmpeg_archive, 'r') as zip_ref:
|
|
204
|
+
zip_ref.extractall(ffmpeg_dir)
|
|
205
|
+
|
|
206
|
+
# Flatten directory structure - move all files to root ffmpeg_dir
|
|
207
|
+
def flatten_directory(directory):
|
|
208
|
+
for root, dirs, files in os.walk(directory):
|
|
209
|
+
for file in files:
|
|
210
|
+
file_path = os.path.join(root, file)
|
|
211
|
+
if root != directory: # Only move files from subdirectories
|
|
212
|
+
target_path = os.path.join(directory, file)
|
|
213
|
+
# Handle name conflicts by keeping the first occurrence
|
|
214
|
+
if not os.path.exists(target_path):
|
|
215
|
+
shutil.move(file_path, target_path)
|
|
216
|
+
# Remove empty subdirectories
|
|
217
|
+
for root, dirs, files in os.walk(directory, topdown=False):
|
|
218
|
+
for dir_name in dirs:
|
|
219
|
+
dir_path = os.path.join(root, dir_name)
|
|
220
|
+
try:
|
|
221
|
+
os.rmdir(dir_path)
|
|
222
|
+
except OSError:
|
|
223
|
+
pass # Directory not empty
|
|
224
|
+
|
|
225
|
+
flatten_directory(ffmpeg_dir)
|
|
194
226
|
|
|
195
227
|
# Copy ffmpeg.exe to the python folder
|
|
196
228
|
if os.path.exists(ffmpeg_exe_path):
|
|
197
229
|
shutil.copy2(ffmpeg_exe_path, ffmpeg_in_python)
|
|
198
230
|
logger.info(f"Copied ffmpeg.exe to Python folder: {ffmpeg_in_python}")
|
|
199
231
|
else:
|
|
200
|
-
logger.warning(f"ffmpeg.exe not found in {ffmpeg_dir}.")
|
|
232
|
+
logger.warning(f"ffmpeg.exe not found in {ffmpeg_dir}. Extraction might have failed.")
|
|
201
233
|
logger.info(f"FFmpeg extracted to {ffmpeg_dir}.")
|
|
202
234
|
|
|
235
|
+
|
|
203
236
|
def download_ocenaudio_if_needed():
|
|
204
237
|
ocenaudio_dir = os.path.join(get_app_directory(), 'ocenaudio')
|
|
205
238
|
ocenaudio_exe_path = os.path.join(ocenaudio_dir, 'ocenaudio.exe')
|
|
@@ -234,4 +267,4 @@ def main():
|
|
|
234
267
|
download_ocenaudio_if_needed()
|
|
235
268
|
|
|
236
269
|
if __name__ == "__main__":
|
|
237
|
-
main()
|
|
270
|
+
main()
|
GameSentenceMiner/util/ffmpeg.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
1
3
|
import subprocess
|
|
4
|
+
import sys
|
|
2
5
|
import tempfile
|
|
3
6
|
import time
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
|
|
6
9
|
from GameSentenceMiner import obs
|
|
10
|
+
from GameSentenceMiner.util.configuration import get_app_directory, is_windows, logger, get_config, \
|
|
11
|
+
get_temporary_directory, gsm_state, is_linux
|
|
7
12
|
from GameSentenceMiner.util.gsm_utils import make_unique_file_name, get_file_modification_time
|
|
8
13
|
from GameSentenceMiner.util import configuration
|
|
9
|
-
from GameSentenceMiner.util.configuration import *
|
|
10
|
-
from GameSentenceMiner.util.model import VADResult
|
|
11
14
|
from GameSentenceMiner.util.text_log import initial_time
|
|
12
15
|
|
|
13
16
|
|
|
@@ -283,6 +283,9 @@ class OverlayProcessor:
|
|
|
283
283
|
if not self.lens:
|
|
284
284
|
logger.error("OCR engines are not initialized. Cannot perform OCR for Overlay.")
|
|
285
285
|
return []
|
|
286
|
+
|
|
287
|
+
if get_config().overlay.scan_delay > 0:
|
|
288
|
+
await asyncio.sleep(get_config().overlay.scan_delay)
|
|
286
289
|
|
|
287
290
|
# 1. Get screenshot
|
|
288
291
|
full_screenshot, monitor_width, monitor_height = self._get_full_screenshot()
|
|
@@ -267,57 +267,3 @@ os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
|
|
|
267
267
|
# f.write(data)
|
|
268
268
|
# except Exception as e:
|
|
269
269
|
# logger.error(f"Failed to fetch JSON from {url}: {e}")
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# Remove GitHub replacements from local OCR replacements file, these replacements are not needed
|
|
273
|
-
def remove_github_replacements_from_local_ocr():
|
|
274
|
-
github_url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/main/electron-src/assets/ocr_replacements.json"
|
|
275
|
-
|
|
276
|
-
github_replacements = {}
|
|
277
|
-
try:
|
|
278
|
-
response = requests.get(github_url)
|
|
279
|
-
response.raise_for_status()
|
|
280
|
-
github_data = response.json()
|
|
281
|
-
github_replacements = github_data.get('args', {}).get('replacements', {})
|
|
282
|
-
logger.debug(f"Successfully fetched {len(github_replacements)} replacements from GitHub.")
|
|
283
|
-
except requests.exceptions.RequestException as e:
|
|
284
|
-
logger.debug(f"Failed to fetch GitHub replacements from {github_url}: {e}")
|
|
285
|
-
return
|
|
286
|
-
except json.JSONDecodeError as e:
|
|
287
|
-
logger.debug(f"Error decoding JSON from GitHub response: {e}")
|
|
288
|
-
return
|
|
289
|
-
|
|
290
|
-
if not os.path.exists(OCR_REPLACEMENTS_FILE):
|
|
291
|
-
logger.debug(f"Local file {OCR_REPLACEMENTS_FILE} does not exist. No replacements to remove.")
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
try:
|
|
295
|
-
with open(OCR_REPLACEMENTS_FILE, 'r', encoding='utf-8') as f:
|
|
296
|
-
local_ocr_data = json.load(f)
|
|
297
|
-
|
|
298
|
-
local_replacements = local_ocr_data.get('args', {}).get('replacements', {})
|
|
299
|
-
original_count = len(local_replacements)
|
|
300
|
-
logger.debug(f"Loaded {original_count} replacements from local file.")
|
|
301
|
-
|
|
302
|
-
removed_count = 0
|
|
303
|
-
for key_to_remove in github_replacements.keys():
|
|
304
|
-
if key_to_remove in local_replacements:
|
|
305
|
-
del local_replacements[key_to_remove]
|
|
306
|
-
removed_count += 1
|
|
307
|
-
|
|
308
|
-
if removed_count > 0:
|
|
309
|
-
local_ocr_data['args']['replacements'] = local_replacements
|
|
310
|
-
with open(OCR_REPLACEMENTS_FILE, 'w', encoding='utf-8') as f:
|
|
311
|
-
json.dump(local_ocr_data, f, ensure_ascii=False, indent=4)
|
|
312
|
-
logger.debug(f"Successfully removed {removed_count} replacements from {OCR_REPLACEMENTS_FILE}.")
|
|
313
|
-
logger.debug(f"Remaining replacements in local file: {len(local_replacements)}")
|
|
314
|
-
else:
|
|
315
|
-
logger.debug("No matching replacements from GitHub found in your local file to remove.")
|
|
316
|
-
|
|
317
|
-
except json.JSONDecodeError as e:
|
|
318
|
-
logger.debug(f"Error decoding JSON from {OCR_REPLACEMENTS_FILE}: {e}. Please ensure it's valid JSON.")
|
|
319
|
-
except Exception as e:
|
|
320
|
-
logger.debug(f"An unexpected error occurred while processing {OCR_REPLACEMENTS_FILE}: {e}")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
remove_github_replacements_from_local_ocr()
|
GameSentenceMiner/vad.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
1
5
|
import tempfile
|
|
2
6
|
import time
|
|
3
7
|
import warnings
|
|
4
|
-
import requests
|
|
5
8
|
from abc import abstractmethod, ABC
|
|
6
9
|
|
|
7
10
|
from GameSentenceMiner.util import configuration, ffmpeg
|
|
8
|
-
from GameSentenceMiner.util.configuration import
|
|
11
|
+
from GameSentenceMiner.util.configuration import get_config, get_temporary_directory, logger, SILERO, WHISPER
|
|
9
12
|
from GameSentenceMiner.util.ffmpeg import get_audio_length
|
|
10
13
|
from GameSentenceMiner.util.gsm_utils import make_unique_file_name, run_new_thread
|
|
11
14
|
from GameSentenceMiner.util.model import VADResult
|
|
@@ -795,9 +795,20 @@ def register_database_api_routes(app):
|
|
|
795
795
|
punctionation_regex = regex.compile(r'[\p{P}\p{S}\p{Z}]')
|
|
796
796
|
# Get optional year filter parameter
|
|
797
797
|
filter_year = request.args.get('year', None)
|
|
798
|
+
|
|
799
|
+
# Get Start and End time as unix timestamp
|
|
800
|
+
start_timestamp = request.args.get('start', None)
|
|
801
|
+
end_timestamp = request.args.get('end', None)
|
|
798
802
|
|
|
803
|
+
# Convert timestamps to float if provided
|
|
804
|
+
start_timestamp = float(start_timestamp) if start_timestamp else None
|
|
805
|
+
end_timestamp = float(end_timestamp) if end_timestamp else None
|
|
806
|
+
|
|
807
|
+
# # 1. Fetch all lines and sort them chronologically
|
|
808
|
+
# all_lines = sorted(GameLinesTable.all(), key=lambda line: line.timestamp)
|
|
809
|
+
|
|
799
810
|
# 1. Fetch all lines and sort them chronologically
|
|
800
|
-
all_lines =
|
|
811
|
+
all_lines = GameLinesTable.get_lines_filtered_by_timestamp(start=start_timestamp, end=end_timestamp)
|
|
801
812
|
|
|
802
813
|
if not all_lines:
|
|
803
814
|
return jsonify({"labels": [], "datasets": []})
|
|
@@ -78,7 +78,7 @@ class WebsocketServerThread(threading.Thread):
|
|
|
78
78
|
while True:
|
|
79
79
|
try:
|
|
80
80
|
self.server = start_server = websockets.serve(self.server_handler,
|
|
81
|
-
|
|
81
|
+
get_config().advanced.localhost_bind_address,
|
|
82
82
|
self.get_ws_port_func(),
|
|
83
83
|
max_size=1000000000)
|
|
84
84
|
async with start_server:
|
|
@@ -929,4 +929,24 @@ h1 {
|
|
|
929
929
|
.games-table td:nth-child(6) {
|
|
930
930
|
display: none;
|
|
931
931
|
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
input[type="date"].dashboard-date-input {
|
|
935
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
936
|
+
transition: all 0.2s ease; /* smooth animation */
|
|
937
|
+
border-radius: 8px;
|
|
938
|
+
border: 1px solid var(--border-color);
|
|
939
|
+
padding: 8px 12px;
|
|
940
|
+
font-size: 14px;
|
|
941
|
+
font-weight: 500;
|
|
942
|
+
color: var(--text-primary);
|
|
943
|
+
background: var(--bg-tertiary);
|
|
944
|
+
cursor: pointer;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
input[type="date"].dashboard-date-input:focus,
|
|
948
|
+
input[type="date"].dashboard-date-input:hover {
|
|
949
|
+
border-color: var(--accent-color);
|
|
950
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
|
|
951
|
+
outline: none;
|
|
932
952
|
}
|