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.

Files changed (31) hide show
  1. GameSentenceMiner/anki.py +31 -3
  2. GameSentenceMiner/config_gui.py +26 -2
  3. GameSentenceMiner/gametext.py +4 -3
  4. GameSentenceMiner/gsm.py +19 -23
  5. GameSentenceMiner/obs.py +17 -7
  6. GameSentenceMiner/ocr/owocr_helper.py +11 -8
  7. GameSentenceMiner/owocr/owocr/run.py +11 -5
  8. GameSentenceMiner/util/configuration.py +7 -5
  9. GameSentenceMiner/util/db.py +176 -8
  10. GameSentenceMiner/util/downloader/download_tools.py +57 -24
  11. GameSentenceMiner/util/ffmpeg.py +5 -2
  12. GameSentenceMiner/util/get_overlay_coords.py +3 -0
  13. GameSentenceMiner/util/gsm_utils.py +0 -54
  14. GameSentenceMiner/vad.py +5 -2
  15. GameSentenceMiner/web/database_api.py +12 -1
  16. GameSentenceMiner/web/gsm_websocket.py +1 -1
  17. GameSentenceMiner/web/static/css/shared.css +20 -0
  18. GameSentenceMiner/web/static/css/stats.css +496 -1
  19. GameSentenceMiner/web/static/js/anki_stats.js +87 -3
  20. GameSentenceMiner/web/static/js/shared.js +2 -49
  21. GameSentenceMiner/web/static/js/stats.js +274 -39
  22. GameSentenceMiner/web/templates/anki_stats.html +36 -0
  23. GameSentenceMiner/web/templates/index.html +1 -1
  24. GameSentenceMiner/web/templates/stats.html +35 -15
  25. GameSentenceMiner/web/texthooking_page.py +31 -8
  26. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/METADATA +1 -1
  27. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/RECORD +31 -31
  28. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/WHEEL +0 -0
  29. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/entry_points.txt +0 -0
  30. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/licenses/LICENSE +0 -0
  31. {gamesentenceminer-2.17.0.dist-info → gamesentenceminer-2.17.2.dist-info}/top_level.txt +0 -0
@@ -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', 'timestamp', 'screenshot_in_anki',
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, 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
- def get_db_directory():
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
- return os.path.join(config_dir, 'gsm.db')
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 GameLinesDB with random Japanese text.")
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
- asset['name'].endswith('Ubuntu-24.04-x86_64.deb')),
86
- "Darwin": next(asset['browser_download_url'] for asset in latest_release['assets'] if
87
- asset['name'].endswith('macOS-Intel.dmg'))
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
- ffmpeg_url = {
167
- "Windows": "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
168
- "Linux": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
169
- "Darwin": "https://evermeet.cx/ffmpeg/ffmpeg.zip"
170
- }.get(platform.system(), None)
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.zip")
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
- with zipfile.ZipFile(ffmpeg_archive, 'r') as zip_ref:
187
- for member in zip_ref.namelist():
188
- filename = os.path.basename(member)
189
- if filename: # Skip directories
190
- source = zip_ref.open(member)
191
- target = open(os.path.join(ffmpeg_dir, filename), "wb")
192
- with source, target:
193
- shutil.copyfileobj(source, target)
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()
@@ -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 = sorted(GameLinesTable.all(), key=lambda line: line.timestamp)
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
- "0.0.0.0",
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
  }