GameSentenceMiner 2.9.10__py3-none-any.whl → 2.9.12__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.
@@ -10,6 +10,7 @@ from google.generativeai import GenerationConfig
10
10
  from groq import Groq
11
11
 
12
12
  from GameSentenceMiner.util.configuration import get_config, Ai, logger
13
+ from GameSentenceMiner.util.gsm_utils import is_connected
13
14
  from GameSentenceMiner.util.text_log import GameLine
14
15
 
15
16
  # Suppress debug logs from httpcore
@@ -184,6 +185,9 @@ current_ai_config: Ai | None = None
184
185
  def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = ""):
185
186
  global ai_manager, current_ai_config
186
187
  try:
188
+ if not is_connected():
189
+ logger.error("No internet connection. Unable to proceed with AI prompt.")
190
+ return ""
187
191
  if not ai_manager or get_config().ai != current_ai_config:
188
192
  if get_config().ai.provider == AIType.GEMINI.value:
189
193
  ai_manager = GeminiAI(model=get_config().ai.gemini_model, api_key=get_config().ai.gemini_api_key, logger=logger)
GameSentenceMiner/anki.py CHANGED
@@ -8,7 +8,8 @@ from requests import post
8
8
 
9
9
  from GameSentenceMiner import obs
10
10
  from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
11
- from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue
11
+ from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue, \
12
+ run_new_thread, open_audio_in_external
12
13
  from GameSentenceMiner.util import ffmpeg, notification
13
14
  from GameSentenceMiner.util.configuration import *
14
15
  from GameSentenceMiner.util.configuration import get_config
@@ -39,6 +40,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
39
40
  if not reuse_audio:
40
41
  if update_audio:
41
42
  audio_in_anki = store_media_file(audio_path)
43
+ if get_config().audio.external_tool and get_config().audio.external_tool_enabled:
44
+ open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
42
45
  if update_picture:
43
46
  logger.info("Getting Screenshot...")
44
47
  screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
@@ -92,12 +95,7 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
92
95
  tag_string = " ".join(tags)
93
96
  invoke("addTags", tags=tag_string, notes=[last_note.noteId])
94
97
 
95
- check_and_update_note(last_note, note, tags)
96
-
97
- if get_config().features.notify_on_update:
98
- notification.send_note_updated(tango)
99
- if get_config().audio.external_tool and get_config().audio.external_tool_enabled and update_audio:
100
- open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
98
+ run_new_thread(lambda: check_and_update_note(last_note, note, tags))
101
99
 
102
100
  def check_and_update_note(last_note, note, tags=[]):
103
101
  selected_notes = invoke("guiSelectedNotes")
@@ -110,13 +108,8 @@ def check_and_update_note(last_note, note, tags=[]):
110
108
  notification.open_browser_window(last_note.noteId, get_config().features.browser_query)
111
109
  if get_config().features.open_anki_edit:
112
110
  notification.open_anki_card(last_note.noteId)
113
-
114
- def open_audio_in_external(fileabspath, shell=False):
115
- logger.info(f"Opening audio in external program...")
116
- if shell:
117
- subprocess.Popen(f' "{get_config().audio.external_tool}" "{fileabspath}" ', shell=True)
118
- else:
119
- subprocess.Popen([get_config().audio.external_tool, fileabspath])
111
+ if get_config().features.notify_on_update:
112
+ notification.send_note_updated(last_note.noteId)
120
113
 
121
114
 
122
115
  def add_image_to_card(last_note: AnkiCard, image_path):
@@ -135,7 +128,7 @@ def add_image_to_card(last_note: AnkiCard, image_path):
135
128
  if update_picture:
136
129
  note['fields'][get_config().anki.picture_field] = image_html
137
130
 
138
- check_and_update_note(last_note, note)
131
+ run_new_thread(lambda: check_and_update_note(last_note, note))
139
132
 
140
133
  logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note.noteId}")
141
134
 
@@ -311,7 +304,7 @@ def update_new_card():
311
304
  def sentence_is_same_as_previous(last_card):
312
305
  if not gsm_state.last_mined_line:
313
306
  return False
314
- return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(gsm_state.last_mined_line)
307
+ return gsm_state.last_mined_line.id == get_mined_line(last_card).id
315
308
 
316
309
  def get_sentence(card):
317
310
  return card.get_field(get_config().anki.sentence_field)
@@ -199,6 +199,7 @@ class ConfigApp:
199
199
  external_tool = self.external_tool.get(),
200
200
  anki_media_collection=self.anki_media_collection.get(),
201
201
  external_tool_enabled=self.external_tool_enabled.get(),
202
+ pre_vad_end_offset=float(self.pre_vad_audio_offset.get()),
202
203
  ),
203
204
  obs=OBS(
204
205
  enabled=self.obs_enabled.get(),
@@ -444,7 +445,7 @@ class ConfigApp:
444
445
 
445
446
  ttk.Label(vad_frame, text="Whisper Model:").grid(row=self.current_row, column=0, sticky='W')
446
447
  self.whisper_model = ttk.Combobox(vad_frame, values=[WHISPER_TINY, WHISPER_BASE, WHISPER_SMALL, WHISPER_MEDIUM,
447
- WHSIPER_LARGE])
448
+ WHSIPER_LARGE, WHISPER_TURBO])
448
449
  self.whisper_model.set(self.settings.vad.whisper_model)
449
450
  self.whisper_model.grid(row=self.current_row, column=1)
450
451
  self.add_label_and_increment_row(vad_frame, "Select the Whisper model size for VAD.", row=self.current_row,
@@ -484,6 +485,13 @@ class ConfigApp:
484
485
  self.vad_beginning_offset.grid(row=self.current_row, column=1)
485
486
  self.add_label_and_increment_row(vad_frame, 'Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning', row=self.current_row, column=2)
486
487
 
488
+ ttk.Label(vad_frame, text="Audio End Offset:").grid(row=self.current_row, column=0, sticky='W')
489
+ self.end_offset = ttk.Entry(vad_frame)
490
+ self.end_offset.insert(0, str(self.settings.audio.end_offset))
491
+ self.end_offset.grid(row=self.current_row, column=1)
492
+ self.add_label_and_increment_row(vad_frame, "Offset in seconds from end of the video to extract.",
493
+ row=self.current_row, column=2)
494
+
487
495
  ttk.Label(vad_frame, text="Add Audio on No Results:").grid(row=self.current_row, column=0, sticky='W')
488
496
  self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
489
497
  ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results).grid(row=self.current_row, column=1, sticky='W')
@@ -890,19 +898,21 @@ class ConfigApp:
890
898
  self.audio_extension.grid(row=self.current_row, column=1)
891
899
  self.add_label_and_increment_row(audio_frame, "File extension for audio files.", row=self.current_row, column=2)
892
900
 
893
- ttk.Label(audio_frame, text="Beginning Offset:").grid(row=self.current_row, column=0, sticky='W')
901
+
902
+ ttk.Label(audio_frame, text="Audio Extraction Beginning Offset:").grid(row=self.current_row, column=0, sticky='W')
894
903
  self.beginning_offset = ttk.Entry(audio_frame)
895
904
  self.beginning_offset.insert(0, str(self.settings.audio.beginning_offset))
896
905
  self.beginning_offset.grid(row=self.current_row, column=1)
897
- self.add_label_and_increment_row(audio_frame, "Offset in seconds to start audio processing.",
906
+ self.add_label_and_increment_row(audio_frame, "Offset in seconds from beginning of the video to extract",
898
907
  row=self.current_row, column=2)
899
908
 
900
- ttk.Label(audio_frame, text="End Offset:").grid(row=self.current_row, column=0, sticky='W')
901
- self.end_offset = ttk.Entry(audio_frame)
902
- self.end_offset.insert(0, str(self.settings.audio.end_offset))
903
- self.end_offset.grid(row=self.current_row, column=1)
904
- self.add_label_and_increment_row(audio_frame, "Offset in seconds to end audio processing.",
905
- row=self.current_row, column=2)
909
+ ttk.Label(audio_frame, text="Audio Extraction End Offset:").grid(row=self.current_row, column=0, sticky='W')
910
+ self.pre_vad_audio_offset = ttk.Entry(audio_frame)
911
+ self.pre_vad_audio_offset.insert(0, str(self.settings.audio.pre_vad_end_offset))
912
+ self.pre_vad_audio_offset.grid(row=self.current_row, column=1)
913
+ self.add_label_and_increment_row(audio_frame, "Offset in seconds to trim from the end before VAD processing starts. Negative = Less time on the end of the pre-vad trimmed audio (should usually be negative)",
914
+ row=self.current_row, column=2)
915
+
906
916
 
907
917
  ttk.Label(audio_frame, text="FFmpeg Preset Options:").grid(row=self.current_row, column=0, sticky='W')
908
918
 
GameSentenceMiner/gsm.py CHANGED
@@ -2,7 +2,12 @@ import asyncio
2
2
  import subprocess
3
3
  import sys
4
4
 
5
- from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
5
+ import os
6
+
7
+ os.environ.pop('TCL_LIBRARY', None)
8
+
9
+ from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread, \
10
+ open_audio_in_external
6
11
  from GameSentenceMiner.util.communication.send import send_restart_signal
7
12
  from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
8
13
  from GameSentenceMiner.vad import vad_processor, VADResult
@@ -13,12 +18,12 @@ try:
13
18
  from subprocess import Popen
14
19
 
15
20
  import keyboard
16
- import psutil
17
21
  import ttkbootstrap as ttk
18
22
  from PIL import Image, ImageDraw
19
23
  from pystray import Icon, Menu, MenuItem
20
24
  from watchdog.events import FileSystemEventHandler
21
25
  from watchdog.observers import Observer
26
+ import psutil
22
27
 
23
28
 
24
29
  from GameSentenceMiner import anki
@@ -86,7 +91,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
86
91
  logger.info("Replay buffer initiated externally. Skipping processing.")
87
92
  return
88
93
  with gsm_state.lock:
89
- gsm_state.last_mined_line = anki.get_sentence(last_note)
94
+ mined_line = get_text_event(last_note)
95
+ gsm_state.last_mined_line = mined_line
90
96
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
91
97
  logger.debug(f"Video found and is readable: {video_path}")
92
98
  if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
@@ -104,7 +110,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
104
110
  last_note = anki.get_cards_by_sentence(gametext.current_line_after_regex)
105
111
  line_cutoff = None
106
112
  start_line = None
107
- mined_line = get_text_event(last_note)
108
113
  if mined_line:
109
114
  start_line = mined_line
110
115
  if mined_line.next:
@@ -151,12 +156,14 @@ class VideoToAudioHandler(FileSystemEventHandler):
151
156
  # timing_only=True) ,doing_multi_line=bool(selected_lines), previous_line=True)
152
157
 
153
158
  if get_config().anki.update_anki and last_note:
154
- anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
159
+ anki.update_anki_card(
160
+ last_note, note, audio_path=final_audio_output, video_path=video_path,
155
161
  tango=tango,
156
162
  should_update_audio=vad_result.success,
157
163
  ss_time=ss_timing,
158
164
  game_line=start_line,
159
- selected_lines=selected_lines)
165
+ selected_lines=selected_lines
166
+ )
160
167
  elif get_config().features.notify_on_update and vad_result.success:
161
168
  notification.send_audio_generated_notification(vad_trimmed_audio)
162
169
  except Exception as e:
@@ -268,7 +275,7 @@ def play_video_in_external(line, filepath):
268
275
 
269
276
  command = [get_config().advanced.video_player_path]
270
277
 
271
- start, _, _ = get_video_timings(filepath, line)
278
+ start, _, _, _ = get_video_timings(filepath, line)
272
279
 
273
280
  if start:
274
281
  if "vlc" in get_config().advanced.video_player_path:
@@ -708,4 +715,3 @@ if __name__ == "__main__":
708
715
  asyncio.run(async_main())
709
716
  except Exception as e:
710
717
  logger.exception(e)
711
- time.sleep(5)
@@ -211,7 +211,7 @@ def do_second_ocr(ocr1_text, time, img, filtering, scrolling=False):
211
211
  engine=ocr2)
212
212
  if scrolling:
213
213
  return text
214
- if fuzz.ratio(last_ocr2_result, text) >= 80:
214
+ if fuzz.ratio(last_ocr2_result, text) >= 90:
215
215
  logger.info("Seems like the same text from previous ocr2 result, not sending")
216
216
  return
217
217
  save_result_image(img)
@@ -280,6 +280,8 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
280
280
  last_oneocr_time = None
281
281
  return
282
282
  if not text or force_stable:
283
+ # or FUTURE ATTEMPT, I THINK THIS IS CLOSE?
284
+ # (orig_text and previous_text and len(orig_text) == len(previous_text_list) and len(orig_text[0] < len(previous_text_list)))):
283
285
  force_stable = False
284
286
  if previous_text and text_stable_start_time:
285
287
  stable_time = text_stable_start_time
@@ -763,6 +763,13 @@ class OneOCR:
763
763
 
764
764
  def __call__(self, img):
765
765
  img = input_to_pil_image(img)
766
+ if img.width < 51 or img.height < 51:
767
+ new_width = max(img.width, 51)
768
+ new_height = max(img.height, 51)
769
+ new_img = Image.new("RGBA", (new_width, new_height), (0, 0, 0, 0))
770
+ new_img.paste(img, ((new_width - img.width) // 2, (new_height - img.height) // 2))
771
+ img = new_img
772
+
766
773
  if not img:
767
774
  return (False, 'Invalid image provided')
768
775
  crop_coords = None
@@ -381,11 +381,15 @@ class TextFiltering:
381
381
  else:
382
382
  orig_text_filtered.append(None)
383
383
 
384
- if isinstance(last_result, list):
385
- last_text = last_result
386
- elif last_result and last_result[1] == engine_index:
387
- last_text = last_result[0]
388
- else:
384
+ try:
385
+ if isinstance(last_result, list):
386
+ last_text = last_result
387
+ elif last_result and last_result[1] == engine_index:
388
+ last_text = last_result[0]
389
+ else:
390
+ last_text = []
391
+ except Exception as e:
392
+ logger.error(f"Error processing last_result {last_result}: {e}")
389
393
  last_text = []
390
394
 
391
395
  new_blocks = []
@@ -895,7 +899,6 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
895
899
  result = engine_instance(img_or_path)
896
900
  res, text, crop_coords = (*result, None)[:3]
897
901
 
898
- end_time = time.time()
899
902
 
900
903
  if not res and ocr_2 == engine:
901
904
  logger.opt(ansi=True).info(f"<{engine_color}>{engine_instance.readable_name}</{engine_color}> failed with message: {text}, trying <{engine_color}>{ocr_1}</{engine_color}>")
@@ -905,9 +908,13 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
905
908
  if last_result:
906
909
  last_result = []
907
910
  break
911
+ start_time = time.time()
908
912
  result = engine_instance(img_or_path)
909
913
  res, text, crop_coords = (*result, None)[:3]
910
914
 
915
+ end_time = time.time()
916
+
917
+
911
918
  orig_text = []
912
919
  # print(filtering)
913
920
  #
@@ -29,6 +29,7 @@ WHISPER_BASE = 'base'
29
29
  WHISPER_SMALL = 'small'
30
30
  WHISPER_MEDIUM = 'medium'
31
31
  WHSIPER_LARGE = 'large'
32
+ WHISPER_TURBO = 'turbo'
32
33
 
33
34
  AI_GEMINI = 'Gemini'
34
35
  AI_GROQ = 'Groq'
@@ -164,6 +165,7 @@ class Audio:
164
165
  extension: str = 'opus'
165
166
  beginning_offset: float = 0.0
166
167
  end_offset: float = 0.5
168
+ pre_vad_end_offset: float = 0.0
167
169
  ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
168
170
  external_tool: str = ""
169
171
  anki_media_collection: str = ""
@@ -200,7 +202,7 @@ class VAD:
200
202
  do_vad_postprocessing: bool = True
201
203
  language: str = 'ja'
202
204
  vosk_url: str = VOSK_BASE
203
- selected_vad_model: str = SILERO
205
+ selected_vad_model: str = WHISPER
204
206
  backup_vad_model: str = OFF
205
207
  trim_beginning: bool = False
206
208
  beginning_offset: float = -0.25
@@ -630,11 +632,14 @@ console_handler.setFormatter(formatter)
630
632
  logger.addHandler(console_handler)
631
633
 
632
634
  # Create rotating file handler with level DEBUG
633
- if 'gsm' in sys.argv[0].lower() or 'gamesentenceminer' in sys.argv[0].lower():
634
- file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
635
- file_handler.setLevel(logging.DEBUG)
636
- file_handler.setFormatter(formatter)
637
- logger.addHandler(file_handler)
635
+ file_path = get_log_path()
636
+ if os.path.exists(file_path) and os.path.getsize(file_path) > 10 * 1024 * 1024:
637
+ shutil.move(os.path.join(file_path, "gamesentenceminer.log"), os.path.join(file_path, "gamesentenceminer_old.log"))
638
+
639
+ file_handler = logging.FileHandler(file_path, encoding='utf-8')
640
+ file_handler.setLevel(logging.DEBUG)
641
+ file_handler.setFormatter(formatter)
642
+ logger.addHandler(file_handler)
638
643
 
639
644
  DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
640
645
 
@@ -7,13 +7,13 @@ from dataclasses_json import dataclass_json
7
7
  from GameSentenceMiner.util.configuration import get_app_directory
8
8
 
9
9
 
10
- @dataclass_json
11
- @dataclass
12
- class SteamGame:
13
- id: int
14
- name: str
15
- processName: str
16
- script: str
10
+ # @dataclass_json
11
+ # @dataclass
12
+ # class SteamGame:
13
+ # id: str = ''
14
+ # name: str = ''
15
+ # processName: str = ''
16
+ # script: str = ''
17
17
 
18
18
  @dataclass_json
19
19
  @dataclass
@@ -31,13 +31,13 @@ class VNConfig:
31
31
  launchVNOnStart: str = ""
32
32
  lastVNLaunched: str = ""
33
33
 
34
- @dataclass_json
35
- @dataclass
36
- class SteamConfig:
37
- steamPath: str = ""
38
- steamGames: List[SteamGame] = field(default_factory=list)
39
- launchSteamOnStart: int = 0
40
- lastGameLaunched: int = 0
34
+ # @dataclass_json
35
+ # @dataclass
36
+ # class SteamConfig:
37
+ # steamPath: str = ""
38
+ # steamGames: List[SteamGame] = field(default_factory=list)
39
+ # launchSteamOnStart: int = 0
40
+ # lastGameLaunched: int = 0
41
41
 
42
42
  @dataclass_json
43
43
  @dataclass
@@ -60,7 +60,7 @@ class StoreConfig:
60
60
  autoUpdateGSMApp: bool = False
61
61
  pythonPath: str = ""
62
62
  VN: VNConfig = field(default_factory=VNConfig)
63
- steam: SteamConfig = field(default_factory=SteamConfig)
63
+ # steam: SteamConfig = field(default_factory=SteamConfig)
64
64
  agentPath: str = ""
65
65
  OCR: OCRConfig = field(default_factory=OCRConfig)
66
66
 
@@ -290,12 +290,12 @@ def get_last_steam_game_launched() -> int:
290
290
  def set_last_steam_game_launched(game_id: int):
291
291
  store.set('steam.lastGameLaunched', game_id)
292
292
 
293
- def get_steam_games() -> List[SteamGame]:
294
- steam_games_data = store.get('steam.steamGames')
295
- return [SteamGame.from_dict(game_data) for game_data in steam_games_data] if isinstance(steam_games_data, list) else []
293
+ # def get_steam_games() -> List[SteamGame]:
294
+ # steam_games_data = store.get('steam.steamGames')
295
+ # return [SteamGame.from_dict(game_data) for game_data in steam_games_data] if isinstance(steam_games_data, list) else []
296
296
 
297
- def set_steam_games(games: List[SteamGame]):
298
- store.set('steam.steamGames', [game.to_dict() for game in games])
297
+ # def set_steam_games(games: List[SteamGame]):
298
+ # store.set('steam.steamGames', [game.to_dict() for game in games])
299
299
 
300
300
  # if __name__ == "__main__":
301
301
  # # Example usage:
@@ -298,20 +298,27 @@ def get_video_duration(file_path):
298
298
  def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line, anki_card_creation_time):
299
299
  trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
300
300
  suffix=f".{get_config().audio.extension}").name
301
- start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line, anki_card_creation_time)
301
+ start_trim_time, total_seconds, total_seconds_after_offset, file_length = get_video_timings(video_path, game_line, anki_card_creation_time)
302
302
  end_trim_time = ""
303
303
 
304
304
  ffmpeg_command = ffmpeg_base_command_list + [
305
305
  "-i", untrimmed_audio,
306
306
  "-ss", str(start_trim_time)]
307
307
  if next_line and next_line > game_line.time:
308
- end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
308
+ end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
309
309
  hours, remainder = divmod(end_total_seconds, 3600)
310
310
  minutes, seconds = divmod(remainder, 60)
311
311
  end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
312
312
  ffmpeg_command.extend(['-to', end_trim_time])
313
313
  logger.debug(
314
314
  f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time}")
315
+ elif get_config().audio.pre_vad_end_offset and get_config().audio.pre_vad_end_offset < 0:
316
+ end_total_seconds = file_length + get_config().audio.pre_vad_end_offset
317
+ hours, remainder = divmod(end_total_seconds, 3600)
318
+ minutes, seconds = divmod(remainder, 60)
319
+ end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
320
+ ffmpeg_command.extend(['-to', end_trim_time])
321
+ logger.debug(f"Trimming end of audio to {end_trim_time} due to pre-vad end offset")
315
322
 
316
323
  ffmpeg_command.extend([
317
324
  "-c", "copy", # Using copy to avoid re-encoding, adjust if needed
@@ -349,7 +356,7 @@ def get_video_timings(video_path, game_line, anki_card_creation_time=None):
349
356
  hours, remainder = divmod(total_seconds_after_offset, 3600)
350
357
  minutes, seconds = divmod(remainder, 60)
351
358
  start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
352
- return start_trim_time, total_seconds, total_seconds_after_offset
359
+ return start_trim_time, total_seconds, total_seconds_after_offset, file_length
353
360
 
354
361
 
355
362
  def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
@@ -2,6 +2,7 @@ import json
2
2
  import os
3
3
  import random
4
4
  import re
5
+ import socket
5
6
  import string
6
7
  import subprocess
7
8
  import threading
@@ -220,6 +221,22 @@ def do_text_replacements(text, replacements_json):
220
221
  return text
221
222
 
222
223
 
224
+ def open_audio_in_external(fileabspath, shell=False):
225
+ logger.info(f"Opening audio in external program...")
226
+ if shell:
227
+ subprocess.Popen(f' "{get_config().audio.external_tool}" "{fileabspath}" ', shell=True)
228
+ else:
229
+ subprocess.Popen([get_config().audio.external_tool, fileabspath])
230
+
231
+ def is_connected():
232
+ try:
233
+ # Attempt to connect to a well-known host
234
+ socket.create_connection(("www.google.com", 80), timeout=2)
235
+ return True
236
+ except OSError:
237
+ return False
238
+
239
+
223
240
  TEXT_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'text_replacements.json')
224
241
  OCR_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'ocr_replacements.json')
225
242
  os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
@@ -32,6 +32,15 @@ def open_browser_window(note_id, query=None):
32
32
  }
33
33
 
34
34
  try:
35
+ if query:
36
+ blank_req_data = {
37
+ "action": "guiBrowse",
38
+ "version": 6,
39
+ "params": {
40
+ "query": "refreshing...",
41
+ }
42
+ }
43
+ requests.post(url, json=blank_req_data, headers=headers)
35
44
  response = requests.post(url, json=data, headers=headers)
36
45
  if response.status_code == 200:
37
46
  if query:
@@ -151,7 +151,9 @@ def get_line_and_future_lines(last_note):
151
151
  return found_lines
152
152
 
153
153
 
154
- def get_mined_line(last_note: AnkiCard, lines):
154
+ def get_mined_line(last_note: AnkiCard, lines=None):
155
+ if lines is None:
156
+ lines = []
155
157
  if not last_note:
156
158
  return lines[-1]
157
159
  if not lines:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.9.10
3
+ Version: 2.9.12
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -24,7 +24,7 @@ Requires-Dist: rapidfuzz~=3.9.7
24
24
  Requires-Dist: plyer~=2.1.0
25
25
  Requires-Dist: keyboard~=0.13.5
26
26
  Requires-Dist: websockets~=15.0.1
27
- Requires-Dist: stable-ts~=2.17.5
27
+ Requires-Dist: stable-ts~=2.19.0
28
28
  Requires-Dist: silero-vad~=5.1.2
29
29
  Requires-Dist: ttkbootstrap~=1.10.1
30
30
  Requires-Dist: dataclasses_json~=0.6.7
@@ -37,7 +37,6 @@ Requires-Dist: pygetwindow; sys_platform == "win32"
37
37
  Requires-Dist: flask
38
38
  Requires-Dist: groq
39
39
  Requires-Dist: obsws-python~=1.7.2
40
- Requires-Dist: Flask-SocketIO
41
40
  Dynamic: license-file
42
41
 
43
42
  # GameSentenceMiner (GSM)
@@ -1,12 +1,12 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=pUvu66s6TFM7432bEl0mL4JoPUJiyPUxc1NAEoQngYM,15201
3
- GameSentenceMiner/config_gui.py,sha256=OZJqsU2v03C_MSpw-S6fYQEOXJ_AUvOUCFeT-3aex8Y,82800
2
+ GameSentenceMiner/anki.py,sha256=1DN00SUCSd4adIRrEqirXXf-2jSeFEuYNrr_WB8nlVE,14950
3
+ GameSentenceMiner/config_gui.py,sha256=iAOLD47sQW67kzBcZKSQ0Dwctc1ngZK1lwSVIaLpQPI,83559
4
4
  GameSentenceMiner/gametext.py,sha256=mM-gw1d7c2EEvMUznaAevTQFLswNZavCuxMXhA9pV4g,6251
5
- GameSentenceMiner/gsm.py,sha256=fSMVLyglpqNy8h5NsznWYap-Bu41tVUPwApWUpvSadY,29105
5
+ GameSentenceMiner/gsm.py,sha256=ycv1wMEqTBM919qC5rlr0AVzfPwpYP1yIrbSgfibv4I,29174
6
6
  GameSentenceMiner/obs.py,sha256=O9NYOGu7kwp4flq8LLXp8YJQg0JTZ8qBqiQNQ6u4ku4,14724
7
7
  GameSentenceMiner/vad.py,sha256=Gk_VthD7mDp3-wM_S6bEv8ykGmqzCDbbcRiaEBzAE_o,14835
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- GameSentenceMiner/ai/ai_prompting.py,sha256=tPDiTHlrfZul0hlvEFgZS4V_6oaHkVb-4v79Sd4gtlM,10018
9
+ GameSentenceMiner/ai/ai_prompting.py,sha256=0jBAnngNwmc3dqJiVWe_QRy4Syr-muV-ML2rq0FiUtU,10215
10
10
  GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  GameSentenceMiner/assets/icon.png,sha256=9GRL8uXUAgkUSlvbm9Pv9o2poFVRGdW6s2ub_DeUD9M,937624
12
12
  GameSentenceMiner/assets/icon128.png,sha256=l90j7biwdz5ahwOd5wZ-406ryEV9Pan93dquJQ3e1CI,18395
@@ -19,24 +19,24 @@ GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
19
19
  GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=fEQ2o2NXksGRHpueO8c4TfAp75GEdAtAr1ngTFOsdpg,2257
20
20
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
21
21
  GameSentenceMiner/ocr/owocr_area_selector.py,sha256=71trzwz9Isyy-kN9mLS8vIX-giC8Lkin4slLXaxudac,47162
22
- GameSentenceMiner/ocr/owocr_helper.py,sha256=FQXk5PSCS9gWtcgoIFsPxjVELUwA4Dg1hEX83902K0Q,18114
22
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=upbuBWKGj6VIg0P5lTopZ8kPPu_ErpdIduM21kpvGmI,18310
23
23
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
24
24
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
25
25
  GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
26
26
  GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
27
- GameSentenceMiner/owocr/owocr/ocr.py,sha256=y8RHHaJw8M4BG4CbbtIw0DrV8KP9RjbJNJxjM5v91oU,42236
28
- GameSentenceMiner/owocr/owocr/run.py,sha256=jFN7gYYriHgfqORJiBTz8mPkQsDJ6ZugA0_ATWUxk-U,54750
27
+ GameSentenceMiner/owocr/owocr/ocr.py,sha256=vZR3du1fGg5-3cmPvYKTO4PFk7Lxyf6-BrIy7CmeG0I,42578
28
+ GameSentenceMiner/owocr/owocr/run.py,sha256=U6VIfCvsNPADG3twhp4SQVX1xhihSAGGrBnQj2x0C2c,54964
29
29
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
30
30
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- GameSentenceMiner/util/configuration.py,sha256=gahWrXj4P0DqlbDEuhR2XFfhpqJvh4ndLDA18bnOwBA,25943
32
- GameSentenceMiner/util/electron_config.py,sha256=ZZf54QifdNHbII-JDsMZmdT8nTyrq-7gVvalyLRecfw,9792
33
- GameSentenceMiner/util/ffmpeg.py,sha256=qaCXkfK2fd-1NRqbm7burrdBYgnGx07kBuyenee8Mtk,18697
34
- GameSentenceMiner/util/gsm_utils.py,sha256=RoOTvWCVpmfYA7fLDdIPcgH1c6TZK4jDZq98BectPhg,8272
31
+ GameSentenceMiner/util/configuration.py,sha256=5YJSKelIpBTK7ggpGUERf17xVwH8Hy9GzvJ12eP8kHs,26095
32
+ GameSentenceMiner/util/electron_config.py,sha256=3VmIrcXhC-wIMMc4uqV85NrNenRl4ZUbnQfSjWEwuig,9852
33
+ GameSentenceMiner/util/ffmpeg.py,sha256=daItJprEqi5PQe-aFb836rls3tBHNqIQKz61vlJK07M,19276
34
+ GameSentenceMiner/util/gsm_utils.py,sha256=TLKA4CFAdboE8jOoN0MB0aKS1U5mXcINhOkKbnJqw_g,8802
35
35
  GameSentenceMiner/util/model.py,sha256=iDtLTfR6D-ZC0gCiDqYno6-gA6Z07PZTM4B5MAA6xZI,5704
36
- GameSentenceMiner/util/notification.py,sha256=ViqasUgn9TGqYyaEucb8aatb_6L8kA58EPsMAUYiWUU,3589
36
+ GameSentenceMiner/util/notification.py,sha256=hAUKWWDB4F_9exQhgiajfP5DT8u464RowsJGmBVRN_E,3882
37
37
  GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
38
38
  GameSentenceMiner/util/ss_selector.py,sha256=oCzmDbpEGvVselF-oDPIrBcQktGIZT0Zt16uDLDAHMQ,4493
39
- GameSentenceMiner/util/text_log.py,sha256=JQS0JpcdaTvcdKgfKs3lWskG4dk6NPjqjMJpm2--37I,5310
39
+ GameSentenceMiner/util/text_log.py,sha256=wdFOapIwHzDntR9G0YzJzDcbhO6b1taFkhdgsTnRGJ4,5356
40
40
  GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
41
41
  GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
42
42
  GameSentenceMiner/util/communication/websocket.py,sha256=gPgxA2R2U6QZJjPqbUgODC87gtacPhmuC8lCprIkSmA,3287
@@ -59,9 +59,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
59
59
  GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
60
60
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
61
61
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
62
- gamesentenceminer-2.9.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
- gamesentenceminer-2.9.10.dist-info/METADATA,sha256=WvTTFa-wwzkn21R-ede66gtXCHFCNZCcCNlWi99s6Uw,7251
64
- gamesentenceminer-2.9.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
- gamesentenceminer-2.9.10.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
- gamesentenceminer-2.9.10.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
- gamesentenceminer-2.9.10.dist-info/RECORD,,
62
+ gamesentenceminer-2.9.12.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
+ gamesentenceminer-2.9.12.dist-info/METADATA,sha256=YgJO-mllUkDN7cY9Dq56LrbdBG7op8_1jvB4f9dITgY,7221
64
+ gamesentenceminer-2.9.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ gamesentenceminer-2.9.12.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
+ gamesentenceminer-2.9.12.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
+ gamesentenceminer-2.9.12.dist-info/RECORD,,