GameSentenceMiner 2.9.9__py3-none-any.whl → 2.9.11__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,15 +40,17 @@ 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
- screenshot = ffmpeg.get_screenshot(video_path, ss_time)
47
+ screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
45
48
  wait_for_stable_file(screenshot)
46
49
  screenshot_in_anki = store_media_file(screenshot)
47
50
  if get_config().paths.remove_screenshot:
48
51
  os.remove(screenshot)
49
52
  if get_config().anki.previous_image_field and game_line.prev:
50
- prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev)
53
+ prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev, try_selector=get_config().screenshot.use_screenshot_selector)
51
54
  wait_for_stable_file(prev_screenshot)
52
55
  prev_screenshot_in_anki = store_media_file(prev_screenshot)
53
56
  if get_config().paths.remove_screenshot:
@@ -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)
@@ -184,10 +184,11 @@ class ConfigApp:
184
184
  extension=self.screenshot_extension.get(),
185
185
  custom_ffmpeg_settings=self.screenshot_custom_ffmpeg_settings.get(),
186
186
  screenshot_hotkey_updates_anki=self.screenshot_hotkey_update_anki.get(),
187
- seconds_after_line = self.seconds_after_line.get(),
187
+ seconds_after_line = float(self.seconds_after_line.get()) if self.seconds_after_line.get() else 0.0,
188
188
  # use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get(),
189
189
  # use_new_screenshot_logic=self.use_new_screenshot_logic.get(),
190
- screenshot_timing_setting=self.screenshot_timing.get()
190
+ screenshot_timing_setting=self.screenshot_timing.get(),
191
+ use_screenshot_selector=self.use_screenshot_selector.get(),
191
192
  ),
192
193
  audio=Audio(
193
194
  enabled=self.audio_enabled.get(),
@@ -198,6 +199,7 @@ class ConfigApp:
198
199
  external_tool = self.external_tool.get(),
199
200
  anki_media_collection=self.anki_media_collection.get(),
200
201
  external_tool_enabled=self.external_tool_enabled.get(),
202
+ pre_vad_end_offset=float(self.pre_vad_audio_offset.get()),
201
203
  ),
202
204
  obs=OBS(
203
205
  enabled=self.obs_enabled.get(),
@@ -483,6 +485,13 @@ class ConfigApp:
483
485
  self.vad_beginning_offset.grid(row=self.current_row, column=1)
484
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)
485
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
+
486
495
  ttk.Label(vad_frame, text="Add Audio on No Results:").grid(row=self.current_row, column=0, sticky='W')
487
496
  self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
488
497
  ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results).grid(row=self.current_row, column=1, sticky='W')
@@ -847,6 +856,12 @@ class ConfigApp:
847
856
  self.add_label_and_increment_row(screenshot_frame, "Time in seconds to offset the screenshot based on the Timing setting above (should almost always be positive, can be negative if you use \"middle\")", row=self.current_row,
848
857
  column=2)
849
858
 
859
+ ttk.Label(screenshot_frame, text="Use Screenshot Selector for every card:").grid(row=self.current_row, column=0, sticky='W')
860
+ self.use_screenshot_selector = tk.BooleanVar(value=self.settings.screenshot.use_screenshot_selector)
861
+ ttk.Checkbutton(screenshot_frame, variable=self.use_screenshot_selector).grid(row=self.current_row, column=1, sticky='W')
862
+ self.add_label_and_increment_row(screenshot_frame, "Enable to use the screenshot selector to choose the screenshot point on every card.", row=self.current_row,
863
+ column=2)
864
+
850
865
  # ttk.Label(screenshot_frame, text="Use Beginning of Line as Screenshot:").grid(row=self.current_row, column=0, sticky='W')
851
866
  # self.use_beginning_of_line_as_screenshot = tk.BooleanVar(value=self.settings.screenshot.use_beginning_of_line_as_screenshot)
852
867
  # ttk.Checkbutton(screenshot_frame, variable=self.use_beginning_of_line_as_screenshot).grid(row=self.current_row, column=1, sticky='W')
@@ -883,19 +898,21 @@ class ConfigApp:
883
898
  self.audio_extension.grid(row=self.current_row, column=1)
884
899
  self.add_label_and_increment_row(audio_frame, "File extension for audio files.", row=self.current_row, column=2)
885
900
 
886
- 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')
887
903
  self.beginning_offset = ttk.Entry(audio_frame)
888
904
  self.beginning_offset.insert(0, str(self.settings.audio.beginning_offset))
889
905
  self.beginning_offset.grid(row=self.current_row, column=1)
890
- 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",
891
907
  row=self.current_row, column=2)
892
908
 
893
- ttk.Label(audio_frame, text="End Offset:").grid(row=self.current_row, column=0, sticky='W')
894
- self.end_offset = ttk.Entry(audio_frame)
895
- self.end_offset.insert(0, str(self.settings.audio.end_offset))
896
- self.end_offset.grid(row=self.current_row, column=1)
897
- self.add_label_and_increment_row(audio_frame, "Offset in seconds to end audio processing.",
898
- 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
+
899
916
 
900
917
  ttk.Label(audio_frame, text="FFmpeg Preset Options:").grid(row=self.current_row, column=0, sticky='W')
901
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
  #
@@ -145,6 +145,7 @@ class Screenshot:
145
145
  use_beginning_of_line_as_screenshot: bool = True
146
146
  use_new_screenshot_logic: bool = False
147
147
  screenshot_timing_setting: str = '' # 'middle', 'end'
148
+ use_screenshot_selector: bool = False
148
149
 
149
150
  def __post_init__(self):
150
151
  if not self.screenshot_timing_setting and self.use_beginning_of_line_as_screenshot:
@@ -163,6 +164,7 @@ class Audio:
163
164
  extension: str = 'opus'
164
165
  beginning_offset: float = 0.0
165
166
  end_offset: float = 0.5
167
+ pre_vad_end_offset: float = 0.0
166
168
  ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
167
169
  external_tool: str = ""
168
170
  anki_media_collection: str = ""
@@ -629,11 +631,14 @@ console_handler.setFormatter(formatter)
629
631
  logger.addHandler(console_handler)
630
632
 
631
633
  # Create rotating file handler with level DEBUG
632
- if 'gsm' in sys.argv[0].lower() or 'gamesentenceminer' in sys.argv[0].lower():
633
- file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
634
- file_handler.setLevel(logging.DEBUG)
635
- file_handler.setFormatter(formatter)
636
- logger.addHandler(file_handler)
634
+ file_path = get_log_path()
635
+ if os.path.exists(file_path) and os.path.getsize(file_path) > 10 * 1024 * 1024:
636
+ shutil.move(os.path.join(file_path, "gamesentenceminer.log"), os.path.join(file_path, "gamesentenceminer_old.log"))
637
+
638
+ file_handler = logging.FileHandler(file_path, encoding='utf-8')
639
+ file_handler.setLevel(logging.DEBUG)
640
+ file_handler.setFormatter(formatter)
641
+ logger.addHandler(file_handler)
637
642
 
638
643
  DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
639
644
 
@@ -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,9 +32,21 @@ 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
- logger.info(f"Opened Anki note with ID {note_id}")
46
+ if query:
47
+ logger.info(f"Opened Anki browser with query: {query}")
48
+ else:
49
+ logger.info(f"Opened Anki note in browser with ID {note_id}")
38
50
  else:
39
51
  logger.error(f"Failed to open Anki note with ID {note_id}")
40
52
  except Exception as e:
@@ -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.9
3
+ Version: 2.9.11
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -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=OPamV4p5mSEkfrUdflauzDJ4Uh2rLYtHq9F7D1aMyhY,15077
3
- GameSentenceMiner/config_gui.py,sha256=r-ASCXVNS4Io6Ej3svwC8aJEWc9Rc7u-pzfsAwD4ru8,82079
2
+ GameSentenceMiner/anki.py,sha256=1DN00SUCSd4adIRrEqirXXf-2jSeFEuYNrr_WB8nlVE,14950
3
+ GameSentenceMiner/config_gui.py,sha256=3prvLmQ8LBw5jEYBIelPW_cdmxcpS375tYs4YY7FbSI,83544
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=VCIDRjulbWu-xgU2B_k2i8POVrP-Lw5dN3ghatDcxGs,25901
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=mUhdjtuWJGWzgtcuM3b4zpqlM4m0f8Vl431AjfZfnaw,26070
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=euTnnNDJm0izr0Z5AhZGV2wrrioCASeKUtm5aZFO5zU,3462
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.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
- gamesentenceminer-2.9.9.dist-info/METADATA,sha256=nThHRsiJF7JUpr6q0Y51JndWC6p1zLGwGnxkLtLRhvM,7250
64
- gamesentenceminer-2.9.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
- gamesentenceminer-2.9.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
- gamesentenceminer-2.9.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
- gamesentenceminer-2.9.9.dist-info/RECORD,,
62
+ gamesentenceminer-2.9.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
+ gamesentenceminer-2.9.11.dist-info/METADATA,sha256=PIPovVreLcPokhDBGRaLjZBTxCbQBqk0lxMNjlx9YmU,7221
64
+ gamesentenceminer-2.9.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ gamesentenceminer-2.9.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
+ gamesentenceminer-2.9.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
+ gamesentenceminer-2.9.11.dist-info/RECORD,,