GameSentenceMiner 2.8.13__py3-none-any.whl → 2.8.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GameSentenceMiner/anki.py CHANGED
@@ -282,7 +282,7 @@ def update_new_card():
282
282
  texthooking_page.reset_checked_lines()
283
283
  else:
284
284
  logger.info("New card(s) detected! Added to Processing Queue!")
285
- card_queue.append(last_card)
285
+ card_queue.append((last_card, datetime.now()))
286
286
  try:
287
287
  obs.save_replay_buffer()
288
288
  except Exception as e:
@@ -157,8 +157,9 @@ class ConfigApp:
157
157
  custom_ffmpeg_settings=self.screenshot_custom_ffmpeg_settings.get(),
158
158
  screenshot_hotkey_updates_anki=self.screenshot_hotkey_update_anki.get(),
159
159
  seconds_after_line = self.seconds_after_line.get(),
160
- use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get(),
161
- use_new_screenshot_logic=self.use_new_screenshot_logic.get()
160
+ # use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get(),
161
+ # use_new_screenshot_logic=self.use_new_screenshot_logic.get(),
162
+ screenshot_timing_setting=self.screenshot_timing.get()
162
163
  ),
163
164
  audio=Audio(
164
165
  enabled=self.audio_enabled.get(),
@@ -199,10 +200,11 @@ class ConfigApp:
199
200
  advanced=Advanced(
200
201
  audio_player_path=self.audio_player_path.get(),
201
202
  video_player_path=self.video_player_path.get(),
202
- show_screenshot_buttons=self.show_screenshot_button.get(),
203
+ # show_screenshot_buttons=self.show_screenshot_button.get(),
203
204
  multi_line_line_break=self.multi_line_line_break.get(),
204
205
  multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
205
206
  ocr_sends_to_clipboard=self.ocr_sends_to_clipboard.get(),
207
+ use_anki_note_creation_time=self.use_anki_note_creation_time.get(),
206
208
  ),
207
209
  ai=Ai(
208
210
  enabled=self.ai_enabled.get(),
@@ -754,28 +756,46 @@ class ConfigApp:
754
756
  self.add_label_and_increment_row(screenshot_frame, "Custom FFmpeg options for re-encoding screenshots.",
755
757
  row=self.current_row, column=2)
756
758
 
759
+
757
760
  ttk.Label(screenshot_frame, text="Screenshot Hotkey Updates Anki:").grid(row=self.current_row, column=0, sticky='W')
758
761
  self.screenshot_hotkey_update_anki = tk.BooleanVar(value=self.settings.screenshot.screenshot_hotkey_updates_anki)
759
762
  ttk.Checkbutton(screenshot_frame, variable=self.screenshot_hotkey_update_anki).grid(row=self.current_row, column=1, sticky='W')
760
763
  self.add_label_and_increment_row(screenshot_frame, "Enable to allow Screenshot hotkey/button to update the latest anki card.", row=self.current_row,
761
764
  column=2)
762
765
 
763
- ttk.Label(screenshot_frame, text="Seconds After Line to SS:").grid(row=self.current_row, column=0, sticky='W')
766
+ ttk.Label(screenshot_frame, text="Screenshot Timing:").grid(row=self.current_row, column=0, sticky='W')
767
+ self.screenshot_timing = ttk.Combobox(screenshot_frame, values=['beginning', 'middle', 'end'])
768
+ self.screenshot_timing.insert(0, self.settings.screenshot.screenshot_timing_setting)
769
+ self.screenshot_timing.grid(row=self.current_row, column=1)
770
+ self.add_label_and_increment_row(screenshot_frame, "Select when to take the screenshot relative to the line: beginning, middle, or end.", row=self.current_row,
771
+ column=2)
772
+
773
+ ttk.Label(screenshot_frame, text="Screenshot Offset:").grid(row=self.current_row, column=0, sticky='W')
764
774
  self.seconds_after_line = ttk.Entry(screenshot_frame)
765
775
  self.seconds_after_line.insert(0, str(self.settings.screenshot.seconds_after_line))
766
776
  self.seconds_after_line.grid(row=self.current_row, column=1)
767
- self.add_label_and_increment_row(screenshot_frame, "This is only used for mining from lines from history (not current line)", row=self.current_row,
777
+ 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)", row=self.current_row,
768
778
  column=2)
769
779
 
770
- ttk.Label(screenshot_frame, text="Use Beginning of Line as Screenshot:").grid(row=self.current_row, column=0, sticky='W')
771
- self.use_beginning_of_line_as_screenshot = tk.BooleanVar(value=self.settings.screenshot.use_beginning_of_line_as_screenshot)
772
- ttk.Checkbutton(screenshot_frame, variable=self.use_beginning_of_line_as_screenshot).grid(row=self.current_row, column=1, sticky='W')
773
- self.add_label_and_increment_row(screenshot_frame, "Enable to use the beginning of the line as the screenshot point. Adjust the above setting to fine-tine timing.", row=self.current_row, column=2)
774
-
775
- ttk.Label(screenshot_frame, text="Use alternative screenshot logic:").grid(row=self.current_row, column=0, sticky='W')
776
- self.use_new_screenshot_logic = tk.BooleanVar(value=self.settings.screenshot.use_new_screenshot_logic)
777
- ttk.Checkbutton(screenshot_frame, variable=self.use_new_screenshot_logic).grid(row=self.current_row, column=1, sticky='W')
778
- self.add_label_and_increment_row(screenshot_frame, "Enable to use the new screenshot logic. This will try to take the screenshot in the middle of the voiceline, or middle of the line if no audio/vad.", row=self.current_row, column=2)
780
+ # ttk.Label(screenshot_frame, text="Use Beginning of Line as Screenshot:").grid(row=self.current_row, column=0, sticky='W')
781
+ # self.use_beginning_of_line_as_screenshot = tk.BooleanVar(value=self.settings.screenshot.use_beginning_of_line_as_screenshot)
782
+ # ttk.Checkbutton(screenshot_frame, variable=self.use_beginning_of_line_as_screenshot).grid(row=self.current_row, column=1, sticky='W')
783
+ # self.add_label_and_increment_row(screenshot_frame, "Enable to use the beginning of the line as the screenshot point. Adjust the above setting to fine-tine timing.", row=self.current_row, column=2)
784
+ #
785
+ # ttk.Label(screenshot_frame, text="Use alternative screenshot logic:").grid(row=self.current_row, column=0, sticky='W')
786
+ # self.use_new_screenshot_logic = tk.BooleanVar(value=self.settings.screenshot.use_new_screenshot_logic)
787
+ # ttk.Checkbutton(screenshot_frame, variable=self.use_new_screenshot_logic).grid(row=self.current_row, column=1, sticky='W')
788
+ # self.add_label_and_increment_row(screenshot_frame, "Enable to use the new screenshot logic. This will try to take the screenshot in the middle of the voiceline, or middle of the line if no audio/vad.", row=self.current_row, column=2)
789
+ #
790
+
791
+ def update_audio_ffmpeg_settings(self, event):
792
+ selected_option = self.ffmpeg_preset_options.get()
793
+ if selected_option in self.ffmpeg_preset_options_map:
794
+ self.ffmpeg_reencode_options.delete(0, tk.END)
795
+ self.ffmpeg_reencode_options.insert(0, self.ffmpeg_preset_options_map[selected_option])
796
+ else:
797
+ self.ffmpeg_reencode_options.delete(0, tk.END)
798
+ self.ffmpeg_reencode_options.insert(0, "")
779
799
 
780
800
  @new_tab
781
801
  def create_audio_tab(self):
@@ -807,6 +827,28 @@ class ConfigApp:
807
827
  self.add_label_and_increment_row(audio_frame, "Offset in seconds to end audio processing.",
808
828
  row=self.current_row, column=2)
809
829
 
830
+ ttk.Label(audio_frame, text="FFmpeg Preset Options:").grid(row=self.current_row, column=0, sticky='W')
831
+
832
+ # Define display names and their corresponding values
833
+ self.ffmpeg_preset_options_map = {
834
+ "No Re-encode" : "",
835
+ "Simple loudness normalization (Simplest, Start Here)": "-c:a libopus -f opus -af \"loudnorm=I=-23:LRA=7:TP=-2\"",
836
+ "Downmix to mono with normalization (Recommended(?))": "-c:a libopus -ac 1 -f opus -application voip -apply_phase_inv 0 -af \"loudnorm=I=-23:dual_mono=true\"",
837
+ "Downmix to mono, 30kbps, normalized (Optimal(?))": "-c:a libopus -b:a 30k -ac 1 -f opus -application voip -apply_phase_inv 0 -af \"loudnorm=I=-23:dual_mono=true\"",
838
+
839
+ }
840
+
841
+ # Create a Combobox with display names
842
+ self.ffmpeg_preset_options = ttk.Combobox(audio_frame, values=list(self.ffmpeg_preset_options_map.keys()), width=50)
843
+ # self.ffmpeg_preset_options.set("Downmix to mono with normalization") # Set default display name
844
+ self.ffmpeg_preset_options.grid(row=self.current_row, column=1)
845
+
846
+ # Bind selection to update settings
847
+ self.ffmpeg_preset_options.bind("<<ComboboxSelected>>", self.update_audio_ffmpeg_settings)
848
+
849
+ self.add_label_and_increment_row(audio_frame, "Select a preset FFmpeg option for re-encoding screenshots.",
850
+ row=self.current_row, column=2)
851
+
810
852
  ttk.Label(audio_frame, text="FFmpeg Reencode Options:").grid(row=self.current_row, column=0, sticky='W')
811
853
  self.ffmpeg_reencode_options = ttk.Entry(audio_frame, width=50)
812
854
  self.ffmpeg_reencode_options.insert(0, self.settings.audio.ffmpeg_reencode_options)
@@ -814,6 +856,7 @@ class ConfigApp:
814
856
  self.add_label_and_increment_row(audio_frame, "Custom FFmpeg options for re-encoding audio files.",
815
857
  row=self.current_row, column=2)
816
858
 
859
+
817
860
  ttk.Label(audio_frame, text="Anki Media Collection:").grid(row=self.current_row, column=0, sticky='W')
818
861
  self.anki_media_collection = ttk.Entry(audio_frame)
819
862
  self.anki_media_collection.insert(0, self.settings.audio.anki_media_collection)
@@ -974,11 +1017,6 @@ class ConfigApp:
974
1017
  self.play_latest_audio_hotkey.grid(row=self.current_row, column=1)
975
1018
  self.add_label_and_increment_row(advanced_frame, "Hotkey to trim and play the latest audio.", row=self.current_row, column=2)
976
1019
 
977
- ttk.Label(advanced_frame, text="Show Screenshot Button:").grid(row=self.current_row, column=0, sticky='W')
978
- self.show_screenshot_button = tk.BooleanVar(value=self.settings.advanced.show_screenshot_buttons)
979
- ttk.Checkbutton(advanced_frame, variable=self.show_screenshot_button).grid(row=self.current_row, column=1, sticky='W')
980
- self.add_label_and_increment_row(advanced_frame, "Show the screenshot button in the utility gui.", row=self.current_row, column=2)
981
-
982
1020
  ttk.Label(advanced_frame, text="Multi-line Line-Break:").grid(row=self.current_row, column=0, sticky='W')
983
1021
  self.multi_line_line_break = ttk.Entry(advanced_frame)
984
1022
  self.multi_line_line_break.insert(0, self.settings.advanced.multi_line_line_break)
@@ -997,6 +1035,12 @@ class ConfigApp:
997
1035
  self.add_label_and_increment_row(advanced_frame, "Enable to send OCR results to clipboard.", row=self.current_row, column=2)
998
1036
 
999
1037
 
1038
+ ttk.Label(advanced_frame, text="Use Anki Creation Date for Audio Timing:").grid(row=self.current_row, column=0, sticky='W')
1039
+ self.use_anki_note_creation_time = tk.BooleanVar(value=self.settings.advanced.use_anki_note_creation_time)
1040
+ ttk.Checkbutton(advanced_frame, variable=self.use_anki_note_creation_time).grid(row=self.current_row, column=1, sticky='W')
1041
+ self.add_label_and_increment_row(advanced_frame, "Use the Anki note creation date for audio timing instead of the OBS replay time.", row=self.current_row, column=2)
1042
+
1043
+
1000
1044
 
1001
1045
  @new_tab
1002
1046
  def create_ai_tab(self):
@@ -109,10 +109,21 @@ class Screenshot:
109
109
  quality: str = 85
110
110
  extension: str = "webp"
111
111
  custom_ffmpeg_settings: str = ''
112
+ custom_ffmpeg_option_selected: str = ''
112
113
  screenshot_hotkey_updates_anki: bool = False
113
114
  seconds_after_line: float = 1.0
114
115
  use_beginning_of_line_as_screenshot: bool = True
115
116
  use_new_screenshot_logic: bool = False
117
+ screenshot_timing_setting: str = '' # 'middle', 'end'
118
+
119
+ def __post_init__(self):
120
+ if not self.screenshot_timing_setting and self.use_beginning_of_line_as_screenshot:
121
+ self.screenshot_timing_setting = 'beginning'
122
+ if not self.screenshot_timing_setting and self.use_new_screenshot_logic:
123
+ self.screenshot_timing_setting = 'middle'
124
+ if not self.screenshot_timing_setting and not self.use_beginning_of_line_as_screenshot and not self.use_new_screenshot_logic:
125
+ self.screenshot_timing_setting = 'end'
126
+
116
127
 
117
128
 
118
129
  @dataclass_json
@@ -181,6 +192,7 @@ class Advanced:
181
192
  multi_line_line_break: str = '<br>'
182
193
  multi_line_sentence_storage_field: str = ''
183
194
  ocr_sends_to_clipboard: bool = True
195
+ use_anki_note_creation_time: bool = False
184
196
 
185
197
  @dataclass_json
186
198
  @dataclass
@@ -57,6 +57,8 @@ def get_screenshot_time(video_path, game_line, default_beginning=False, vad_begi
57
57
  # Assuming initial_time is defined elsewhere if game_line is None
58
58
  line_time = initial_time
59
59
 
60
+ logger.info("Calculating screenshot time for line: " + str(game_line.text))
61
+
60
62
  file_length = get_video_duration(video_path)
61
63
  file_mod_time = get_file_modification_time(video_path)
62
64
 
@@ -69,14 +71,27 @@ def get_screenshot_time(video_path, game_line, default_beginning=False, vad_begi
69
71
  if vad_beginning and vad_end and not doing_multi_line:
70
72
  logger.debug("Using VAD to determine screenshot time")
71
73
  screenshot_time_from_beginning = line_timestamp_in_video + vad_end - screenshot_offset
72
- elif get_config().screenshot.use_new_screenshot_logic:
74
+ elif get_config().screenshot.screenshot_timing_setting == "beginning":
75
+ logger.debug("Using beginning of line for screenshot")
76
+ screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
77
+ elif get_config().screenshot.screenshot_timing_setting == "middle":
73
78
  if game_line.next:
74
79
  logger.debug("Finding time between lines for screenshot")
75
80
  screenshot_time_from_beginning = line_timestamp_in_video + ((game_line.next.time - game_line.time).total_seconds() / 2)
76
81
  else:
77
82
  logger.debug("Using end of line for screenshot")
78
83
  screenshot_time_from_beginning = file_length - screenshot_offset
84
+ elif get_config().screenshot.screenshot_timing_setting == "end":
85
+ logger.debug("Using end of line for screenshot")
86
+ if game_line.next:
87
+ logger.debug("Finding time between lines for screenshot")
88
+ screenshot_time_from_beginning = line_timestamp_in_video + (game_line.next.time - game_line.time).total_seconds() - screenshot_offset
89
+ else:
90
+ logger.debug("Using end of video for screenshot")
91
+ # If no next line, use the end of the video
92
+ screenshot_time_from_beginning = file_length - screenshot_offset
79
93
  else:
94
+ logger.error(f"Invalid screenshot timing setting: {get_config().screenshot.screenshot_timing_setting}")
80
95
  screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
81
96
 
82
97
  # Check if the calculated time is out of bounds
@@ -150,7 +165,7 @@ def get_audio_codec(video_path):
150
165
  return None
151
166
 
152
167
 
153
- def get_audio_and_trim(video_path, game_line, next_line_time):
168
+ def get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time):
154
169
  supported_formats = {
155
170
  'opus': 'libopus',
156
171
  'mp3': 'libmp3lame',
@@ -184,7 +199,7 @@ def get_audio_and_trim(video_path, game_line, next_line_time):
184
199
 
185
200
  subprocess.run(command)
186
201
 
187
- return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time)
202
+ return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time, anki_card_creation_time)
188
203
 
189
204
 
190
205
  def get_video_duration(file_path):
@@ -202,10 +217,10 @@ def get_video_duration(file_path):
202
217
  return float(duration_info["format"]["duration"]) # Return the duration in seconds
203
218
 
204
219
 
205
- def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line):
220
+ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line, anki_card_creation_time):
206
221
  trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
207
222
  suffix=f".{get_config().audio.extension}").name
208
- start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line)
223
+ start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line, anki_card_creation_time)
209
224
 
210
225
  ffmpeg_command = ffmpeg_base_command_list + [
211
226
  "-i", untrimmed_audio,
@@ -232,8 +247,11 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
232
247
  logger.info(f"Audio trimmed and saved to {trimmed_audio}")
233
248
  return trimmed_audio
234
249
 
235
- def get_video_timings(video_path, game_line):
236
- file_mod_time = get_file_modification_time(video_path)
250
+ def get_video_timings(video_path, game_line, anki_card_creation_time):
251
+ if anki_card_creation_time and get_config().advanced.use_anki_note_creation_time:
252
+ file_mod_time = anki_card_creation_time
253
+ else:
254
+ file_mod_time = get_file_modification_time(video_path)
237
255
  file_length = get_video_duration(video_path)
238
256
  time_delta = file_mod_time - game_line.time
239
257
  # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
@@ -78,13 +78,15 @@ async def listen_websocket():
78
78
  if e.response.status_code == 404:
79
79
  logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
80
80
  try_other = True
81
- if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
82
- logger.error(f"Unexpected error in Texthooker WebSocket connection: {e}")
83
- websocket_connected = False
84
- if not reconnecting:
85
- logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
86
- reconnecting = True
87
- await asyncio.sleep(5)
81
+ await asyncio.sleep(0.1)
82
+ else:
83
+ if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
84
+ logger.error(f"Unexpected error in Texthooker WebSocket connection: {e}")
85
+ websocket_connected = False
86
+ if not reconnecting:
87
+ logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
88
+ reconnecting = True
89
+ await asyncio.sleep(5)
88
90
 
89
91
  async def handle_new_text_event(current_clipboard, line_time=None):
90
92
  global current_line, current_line_time, current_line_after_regex
GameSentenceMiner/gsm.py CHANGED
@@ -90,8 +90,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
90
90
  return
91
91
  try:
92
92
  last_note = None
93
+ anki_card_creation_time = None
93
94
  if anki.card_queue and len(anki.card_queue) > 0:
94
- last_note = anki.card_queue.pop(0)
95
+ last_note, anki_card_creation_time = anki.card_queue.pop(0)
95
96
  with util.lock:
96
97
  util.set_last_mined_line(anki.get_sentence(last_note))
97
98
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
@@ -136,7 +137,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
136
137
  final_audio_output, should_update_audio, vad_trimmed_audio, vad_beginning, vad_end = VideoToAudioHandler.get_audio(
137
138
  start_line,
138
139
  line_cutoff,
139
- video_path)
140
+ video_path,
141
+ anki_card_creation_time)
140
142
  else:
141
143
  final_audio_output = ""
142
144
  should_update_audio = False
@@ -148,7 +150,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
148
150
  elif not get_config().anki.sentence_audio_field:
149
151
  logger.info("No SentenceAudio Field in config, skipping audio processing!")
150
152
 
151
- ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_beginning, vad_end, bool(selected_lines))
153
+ ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_beginning=vad_beginning, vad_end=vad_end, doing_multi_line=bool(selected_lines))
152
154
 
153
155
  if get_config().anki.update_anki and last_note:
154
156
  anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
@@ -171,8 +173,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
171
173
 
172
174
 
173
175
  @staticmethod
174
- def get_audio(game_line, next_line_time, video_path, temporary=False):
175
- trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
176
+ def get_audio(game_line, next_line_time, video_path, anki_card_creation_time,temporary=False):
177
+ trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time)
176
178
  if temporary:
177
179
  return trimmed_audio
178
180
  vad_trimmed_audio = make_unique_file_name(
@@ -292,8 +294,6 @@ def get_screenshot():
292
294
  encoded_image = ffmpeg.process_image(image)
293
295
  if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
294
296
  last_note = anki.get_last_anki_card()
295
- if last_note:
296
- logger.debug(json.dumps(last_note))
297
297
  if get_config().features.backfill_audio:
298
298
  last_note = anki.get_cards_by_sentence(gametext.current_line)
299
299
  if last_note:
@@ -306,7 +306,7 @@ def get_screenshot():
306
306
  else:
307
307
  notification.send_screenshot_saved(encoded_image)
308
308
  except Exception as e:
309
- logger.error(f"Failed to get Screenshot {e}")
309
+ logger.error(f"Failed to get Screenshot: {e}")
310
310
 
311
311
 
312
312
  def create_image():
@@ -43,6 +43,7 @@ class OCRConfig:
43
43
  coordinate_system: str = None
44
44
  window_geometry: Optional[WindowGeometry] = None
45
45
  window: Optional[str] = None
46
+ language: str = "ja"
46
47
 
47
48
  def __post_init__(self):
48
49
  if self.coordinate_system and self.coordinate_system == "percentage" and self.window:
@@ -320,7 +320,7 @@ def run_oneocr(ocr_config: OCRConfig, i, area=False):
320
320
  text_callback=text_callback,
321
321
  screen_capture_exclusions=exclusions,
322
322
  rectangle=i,
323
- language="ja")
323
+ language=language)
324
324
  done = True
325
325
 
326
326
 
@@ -335,27 +335,31 @@ def get_window(window_name):
335
335
  else:
336
336
  return None
337
337
  except Exception as e:
338
- print(f"Error finding window '{self.window_name}': {e}")
338
+ print(f"Error finding window '{window_name}': {e}")
339
339
  return None
340
340
 
341
341
  if __name__ == "__main__":
342
- global ocr1, ocr2, twopassocr
342
+ global ocr1, ocr2, twopassocr, language
343
343
  import sys
344
344
 
345
345
  args = sys.argv[1:]
346
- if len(args) == 3:
347
- ocr1 = args[0]
348
- ocr2 = args[1]
349
- twopassocr = bool(int(args[2]))
350
- elif len(args) == 2:
351
- ocr1 = args[0]
352
- ocr2 = args[1]
346
+ if len(args) == 4:
347
+ language = args[0]
348
+ ocr1 = args[1]
349
+ ocr2 = args[2]
350
+ twopassocr = bool(int(args[3]))
351
+ elif len(args) == 3:
352
+ language = args[0]
353
+ ocr1 = args[1]
354
+ ocr2 = args[2]
353
355
  twopassocr = True
354
- elif len(args) == 1:
355
- ocr1 = args[0]
356
+ elif len(args) == 2:
357
+ language = args[0]
358
+ ocr1 = args[1]
356
359
  ocr2 = None
357
360
  twopassocr = False
358
361
  else:
362
+ language = "ja"
359
363
  ocr1 = "oneocr"
360
364
  ocr2 = "glens"
361
365
  twopassocr = True
GameSentenceMiner/util.py CHANGED
@@ -58,7 +58,7 @@ def timedelta_to_ffmpeg_friendly_format(td_obj):
58
58
 
59
59
 
60
60
  def get_file_modification_time(file_path):
61
- mod_time_epoch = os.path.getctime(file_path)
61
+ mod_time_epoch = os.path.getmtime(file_path)
62
62
  mod_time = datetime.fromtimestamp(mod_time_epoch)
63
63
  return mod_time
64
64
 
@@ -255,12 +255,12 @@ os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
255
255
 
256
256
  import urllib.request
257
257
 
258
- if not os.path.exists(OCR_REPLACEMENTS_FILE):
259
- url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/refs/heads/main/electron-src/assets/ocr_replacements.json"
260
- try:
261
- with urllib.request.urlopen(url) as response:
262
- data = response.read().decode('utf-8')
263
- with open(OCR_REPLACEMENTS_FILE, 'w', encoding='utf-8') as f:
264
- f.write(data)
265
- except Exception as e:
266
- logger.error(f"Failed to fetch JSON from {url}: {e}")
258
+ # if not os.path.exists(OCR_REPLACEMENTS_FILE):
259
+ # url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/refs/heads/main/electron-src/assets/ocr_replacements.json"
260
+ # try:
261
+ # with urllib.request.urlopen(url) as response:
262
+ # data = response.read().decode('utf-8')
263
+ # with open(OCR_REPLACEMENTS_FILE, 'w', encoding='utf-8') as f:
264
+ # f.write(data)
265
+ # except Exception as e:
266
+ # logger.error(f"Failed to fetch JSON from {url}: {e}")
@@ -107,26 +107,30 @@
107
107
  </style>
108
108
  </head>
109
109
  <body>
110
+ <div style="position: fixed; top: 20px; right: 20px; display: flex; gap: 10px;">
111
+ <button onclick="window.location.href='/textreplacements'" style="background-color: #1a73e8; color: #ffffff; border: none; padding: 10px 20px; font-size: 12px; cursor: pointer; transition: background-color 0.3s; border-radius: 5px;">
112
+ Text Replacements
113
+ </button>
114
+ <button id="delete-history" style="background-color: #1a73e8; color: #ffffff; border: none; padding: 10px 20px; font-size: 12px; cursor: pointer; transition: background-color 0.3s; border-radius: 5px;">
115
+ Clear History
116
+ </button>
117
+ </div>
110
118
  <div id="initial-events">
111
119
 
112
120
  </div>
113
121
  <hr class="initial-events-separator" id="initial-events-separator" style="display: none;">
114
122
  <div id="session-events">
115
123
 
116
- </div>
117
- <div>
118
- <button onclick="window.location.href='/textreplacements'" style="margin-top: 20px; background-color: #1a73e8; color: #ffffff; border: none; padding: 10px 20px; font-size: 16px; cursor: pointer; transition: background-color 0.3s; border-radius: 5px;">
119
- Text Replacements
120
- </button>
121
124
  </div>
122
125
  <script>
123
126
  let mainStyle = document.querySelector('head style');
127
+ let deleteHistoryButton = document.getElementById('delete-history');
124
128
  console.log(mainStyle);
125
129
  let displayedEventIds = new Set();
126
130
  let isTabActive = true;
127
131
  let isFetching = false; // Flag to track if a fetch is in progress
128
132
  let intervalId = 0;
129
- const fetchInterval = 5000; // Define the interval as a constant
133
+ const fetchInterval = 100; // Define the interval as a constant
130
134
  const websocketPort = {{ websocket_port }} || 55001;
131
135
 
132
136
  // Drag selection variables
@@ -135,11 +139,13 @@
135
139
  let newCheckboxState = false;
136
140
  let hoveredCheckboxes = new Set();
137
141
  let checkboxes = []; // Will hold all checkbox elements
142
+ let checkboxMap = {};
138
143
  let checkboxes_being_updated = new Set();
139
144
 
140
145
  // Shift click selection variable
141
146
  let lastChecked = null;
142
147
 
148
+
143
149
  async function fetchEvents() {
144
150
  if (document.hidden || isFetching) {
145
151
  return;
@@ -152,21 +158,30 @@
152
158
  }
153
159
  const events = await res.json();
154
160
 
161
+ let historyEvents = []
155
162
  events.forEach(ev => {
156
163
  if (!displayedEventIds.has(ev.id)) {
157
164
  if (ev.history) {
158
- addNewEventToHistory(ev)
165
+ historyEvents.push(ev);
159
166
  document.getElementById('initial-events-separator').style.display = 'block';
160
167
  } else {
161
168
  addNewEvent(ev)
162
169
  }
163
170
  }
164
- const checkbox = document.querySelector(`[data-event-id="${ev.id}"]`);
165
- if (checkbox && !checkboxes_being_updated.has(ev.id)) {
166
- checkbox.checked = ev.checked;
171
+ if (!ev.history) {
172
+ console.log(checkboxMap[ev.id])
173
+ if (!checkboxes_being_updated.has(ev.id)) {
174
+ const checkbox = checkboxMap[ev.id];
175
+ if (checkbox) {
176
+ checkbox.checked = ev.checked;
177
+ }
178
+ }
167
179
  }
168
180
  });
169
- checkboxes = Array.from(document.querySelectorAll('#session-events input[type="checkbox"]')); // Update checkboxes array after new events
181
+ if (historyEvents.length > 0) {
182
+ addEventsToHistory(historyEvents);
183
+ }
184
+ // checkboxes = Array.from(document.querySelectorAll('#session-events input[type="checkbox"]')); // Update checkboxes array after new events
170
185
  } catch (error) {
171
186
  console.error("Error fetching events:", error);
172
187
  } finally {
@@ -174,29 +189,32 @@
174
189
  }
175
190
  }
176
191
 
177
- function addNewEventToHistory(event) {
178
- displayedEventIds.add(event.id);
192
+ function addEventsToHistory(events) {
179
193
  const container = document.getElementById('initial-events');
180
- const div = document.createElement('div');
181
- // div.className = 'textline';
194
+ const fragment = document.createDocumentFragment();
182
195
 
183
- const shadowRoot = div.attachShadow({ mode: 'open' });
196
+ events.forEach(event => {
197
+ displayedEventIds.add(event.id);
198
+ const div = document.createElement('div');
199
+ const shadowRoot = div.attachShadow({ mode: 'open' });
184
200
 
185
- const wrapper = document.createElement('div');
186
- wrapper.className = 'textline';
187
- wrapper.innerHTML = `<p>${event.text}</p>
201
+ const wrapper = document.createElement('div');
202
+ wrapper.className = 'textline';
203
+ wrapper.innerHTML = `<p>${event.text}</p>
188
204
  <em class="clock-icon">${event.time.replace(' GMT', '')}</em>
189
205
  `;
190
206
 
191
- const style = document.createElement('style');
192
- style.textContent = mainStyle.innerHTML;
193
- shadowRoot.appendChild(style);
194
- shadowRoot.appendChild(wrapper);
207
+ const style = document.createElement('style');
208
+ style.textContent = mainStyle.innerHTML;
209
+ shadowRoot.appendChild(style);
210
+ shadowRoot.appendChild(wrapper);
195
211
 
196
- container.appendChild(div);
212
+ fragment.appendChild(div);
213
+ });
214
+
215
+ container.appendChild(fragment);
197
216
  window.scrollTo({
198
217
  top: document.documentElement.scrollHeight,
199
- behavior: 'smooth'
200
218
  });
201
219
  }
202
220
 
@@ -235,10 +253,13 @@
235
253
  shadowRoot.appendChild(style);
236
254
  shadowRoot.appendChild(wrapper);
237
255
 
256
+ let checkbox = shadowRoot.querySelector('.multi-line-checkbox')
257
+ checkboxes.push(checkbox);
258
+ checkboxMap[event.id] = checkbox; // Store the checkbox in the map for easy access
259
+
238
260
  container.appendChild(div);
239
261
  window.scrollTo({
240
262
  top: document.documentElement.scrollHeight,
241
- behavior: 'smooth'
242
263
  });
243
264
  }
244
265
 
@@ -344,10 +365,34 @@
344
365
  lastChecked = e.target;
345
366
  }
346
367
 
368
+ function deleteHistory(e) {
369
+ e.preventDefault();
370
+ if (confirm("Are you sure you want to delete the history? This action cannot be undone.")) {
371
+ fetch('/clear_history', {
372
+ method: 'POST',
373
+ headers: { 'Content-Type': 'application/json' }
374
+ })
375
+ .then(response => {
376
+ if (!response.ok) {
377
+ throw new Error(`HTTP error! Status: ${response.status}`);
378
+ }
379
+ // Clear the displayed events
380
+ displayedEventIds.clear();
381
+ document.getElementById('initial-events').innerHTML = '';
382
+ document.getElementById('session-events').innerHTML = '';
383
+ document.getElementById('initial-events-separator').style.display = 'none';
384
+ })
385
+ .catch(error => {
386
+ console.error("Error deleting history:", error);
387
+ });
388
+ }
389
+ }
390
+
347
391
  document.addEventListener('mousedown', handleMouseDown);
348
392
  document.addEventListener('mouseup', handleMouseUp);
349
393
  document.addEventListener('mouseover', handleMouseOver);
350
394
  document.addEventListener('click', handleCheckboxClick);
395
+ deleteHistoryButton.addEventListener('click', deleteHistory);
351
396
 
352
397
  const websocketURL = 'ws://localhost:' + websocketPort;
353
398
  let websocket = {};
@@ -404,7 +449,7 @@
404
449
  return websocket;
405
450
  };
406
451
 
407
- connectWebSocket();
452
+ // connectWebSocket();
408
453
 
409
454
 
410
455
  fetchEvents();
@@ -419,7 +464,6 @@
419
464
 
420
465
  window.scrollTo({
421
466
  top: document.documentElement.scrollHeight,
422
- behavior: 'smooth'
423
467
  });
424
468
  </script>
425
469
  </body>
@@ -103,6 +103,10 @@ class EventManager:
103
103
  event_queue.put(new_event)
104
104
  return new_event
105
105
 
106
+ def reset_checked_lines(self):
107
+ for event in self.events:
108
+ event.checked = False
109
+
106
110
  def get_events(self):
107
111
  return self.events
108
112
 
@@ -117,6 +121,14 @@ class EventManager:
117
121
  if self.conn:
118
122
  self.conn.close()
119
123
 
124
+ def clear_history(self):
125
+ self.cursor.execute("DELETE FROM events WHERE time < ?", (initial_time.isoformat(),))
126
+ logger.info(f"Cleared history before {initial_time.isoformat()}")
127
+ self.conn.commit()
128
+ # Clear the in-memory events as well
129
+ event_manager.events = [event for event in event_manager if not event.history]
130
+ event_manager.events_dict = {event.id: event for event in event_manager.events}
131
+
120
132
  class EventProcessor(threading.Thread):
121
133
  def __init__(self, event_queue, db_path):
122
134
  super().__init__()
@@ -227,6 +239,13 @@ def textreplacements():
227
239
  def get_data():
228
240
  return jsonify([event.to_dict() for event in event_manager])
229
241
 
242
+ @app.route('/clear_history', methods=['POST'])
243
+ def clear_history():
244
+ temp_em = EventManager()
245
+ temp_em.clear_history()
246
+ temp_em.close_connection()
247
+ return jsonify({'message': 'History cleared successfully'}), 200
248
+
230
249
 
231
250
  async def add_event_to_texthooker(line: GameLine):
232
251
  logger.info("Adding event to web server: %s", line.text)
@@ -248,8 +267,7 @@ def update_event():
248
267
  return jsonify({'error': 'Missing id or checked status'}), 400
249
268
 
250
269
  event_manager.get(event_id).checked = checked
251
-
252
- return jsonify({'error': 'Event not found'}), 404
270
+ return jsonify({'message': 'Event updated successfully'}), 200
253
271
 
254
272
  @app.route('/get-screenshot', methods=['Post'])
255
273
  def get_screenshot():
@@ -339,8 +357,7 @@ def are_lines_selected():
339
357
  return any(item.checked for item in event_manager)
340
358
 
341
359
  def reset_checked_lines():
342
- for item in event_manager:
343
- item.checked = False
360
+ event_manager.reset_checked_lines()
344
361
 
345
362
  def open_texthooker():
346
363
  webbrowser.open(url + '/texthooker')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.8.13
3
+ Version: 2.8.15
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,17 +1,17 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=pSkf-il17vKZIBt1ZHHDMfvfO9M-GdF1zox-c--KkAY,14208
3
- GameSentenceMiner/config_gui.py,sha256=6TkDLW-0SPWMeNAwzb5WT7aVJ8mxPqmhAQcS1BHzTik,70979
4
- GameSentenceMiner/configuration.py,sha256=rxl0YocwAF8cCBakOl7k2CPpOfL3FZxnsGMlKZOezmc,21876
2
+ GameSentenceMiner/anki.py,sha256=OCLgZa-iEp93v-R0zKFkDCjule_EAoP5rIqtnMHLnOw,14226
3
+ GameSentenceMiner/config_gui.py,sha256=-xZRTzFtnhnmbSiL03XpNrzV29Pb891nLuXHI_CZw7I,73759
4
+ GameSentenceMiner/configuration.py,sha256=-De0KfXu0jHTPl0xyktUxT-_fNUS_AK7nmzblKXRdGc,22525
5
5
  GameSentenceMiner/electron_config.py,sha256=dGcPYCISPehXubYSzsDuI2Gl092MYK0u3bTnkL9Jh1Y,9787
6
- GameSentenceMiner/ffmpeg.py,sha256=mcEcJnYl06oJGbLaymFUfqClFiHf6Hhf2SXo3UV9tvM,13378
7
- GameSentenceMiner/gametext.py,sha256=ClSpOeohBWG17MRVIbhXfNDnkUdxU9mTspGv9975uEc,5422
8
- GameSentenceMiner/gsm.py,sha256=vCEPg7rE68AUD2u6DesjLxuZSy1gJzbiA0fig673WRM,25254
6
+ GameSentenceMiner/ffmpeg.py,sha256=lLZ9v8_h5wfenLC6W36Ikt0Q7HQrChxsZ6lPk2umXjI,14658
7
+ GameSentenceMiner/gametext.py,sha256=pyIjMxMgKAAIyKzvtq8FV-zdg1ETRf7GVgkE19_2-B4,5513
8
+ GameSentenceMiner/gsm.py,sha256=bwDt7yAM9_Jf0BY0oxs6vwL8jGrHxnw_sznqbJp5Hdg,25382
9
9
  GameSentenceMiner/model.py,sha256=JdnkT4VoPOXmOpRgFdvERZ09c9wLN6tUJxdrKlGZcqo,5305
10
10
  GameSentenceMiner/notification.py,sha256=FY39ChSRK0Y8TQ6lBGsLnpZUFPtFpSy2tweeXVoV7kc,2809
11
11
  GameSentenceMiner/obs.py,sha256=GPlsFrcv1eYelXyJfpspGK0iZK5AXPkoFsIGdB7eJrk,10002
12
12
  GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
13
13
  GameSentenceMiner/text_log.py,sha256=tBuZ8ElUgPtiQV8U6U90kmRxposwIkL3fjOYejdzikc,5153
14
- GameSentenceMiner/util.py,sha256=bb75EQdk4Nf0i9t3XMznjd6NxTWfkOVeE5bZFstbCbU,8910
14
+ GameSentenceMiner/util.py,sha256=LzWGIDZb8NLv-RyrE_d6ycoQEwM1zpaDhWp0LKb6_Zc,8928
15
15
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  GameSentenceMiner/ai/ai_prompting.py,sha256=uTtXIDRpcQqPT4EC-R_pwRP9pBmZ64I6vMDOexhJRp8,9505
17
17
  GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
@@ -22,10 +22,10 @@ GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
22
22
  GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
23
23
  GameSentenceMiner/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
24
24
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=zagsB4UD9mmZX_r6dFBCXZqdDa0XGk-RvIqbKoPB9lQ,1932
25
+ GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=rQC6C8PKJXWoAvwCOYa363kodQQBwl1YNeYsD0bBbx4,1957
26
26
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
27
27
  GameSentenceMiner/ocr/owocr_area_selector.py,sha256=gwYOz-fA5qoL63wh77eyGJtBtO7YVvWyO5cHb3D0Oz4,46738
28
- GameSentenceMiner/ocr/owocr_helper.py,sha256=PCsPbqrECSbi4u8NIq3PJotStaYVBBbfccHu-DrqpwU,17269
28
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=-ezF6AO7ayZBs2vMt0Zj7D-gc8YbrgO69mFsJD7VmRs,17383
29
29
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
30
30
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=r8MI6RAmbkTWqOJ59uvXoDS7CSw5jX5war9ULGWELrA,128
31
31
  GameSentenceMiner/owocr/owocr/config.py,sha256=n-xtVylb2Q_H84jb1ZsIGxPQjTNnyvnRny1RhtaLJM8,7550
@@ -38,7 +38,7 @@ GameSentenceMiner/vad/silero_trim.py,sha256=ULf3zwS-JMsY82cKF7gZxREHw8L6lgpWF2U1
38
38
  GameSentenceMiner/vad/vosk_helper.py,sha256=125X8C9NxFPlWWpoNsbOnEqKx8RCjXN109zNx_QXhyg,6070
39
39
  GameSentenceMiner/vad/whisper_helper.py,sha256=JJ-iltCh813XdjyEw0Wn5DaErf6PDqfH0Efu1Md8cIY,3543
40
40
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- GameSentenceMiner/web/texthooking_page.py,sha256=Xf-m_bNSPUNO5x1u4sYdEtHx6ERF8IXrkF9wDx9pto8,12791
41
+ GameSentenceMiner/web/texthooking_page.py,sha256=AtVV9RS7HC3XnOq4X0FIMqJrzFoGlfSHFvS_CfhuzuA,13558
42
42
  GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
44
44
  GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
@@ -50,10 +50,10 @@ GameSentenceMiner/web/static/web-app-manifest-192x192.png,sha256=EfSNnBmsSaLfESb
50
50
  GameSentenceMiner/web/static/web-app-manifest-512x512.png,sha256=wyqgCWCrLEUxSRXmaA3iJEESd-vM-ZmlTtZFBY4V8Pk,230819
51
51
  GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
53
- GameSentenceMiner/web/templates/utility.html,sha256=8yJvmHqkHoMN06qWDckhGoguWyP7LUVZ2oms0tBRdEw,14297
54
- gamesentenceminer-2.8.13.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
55
- gamesentenceminer-2.8.13.dist-info/METADATA,sha256=EGC_NS8I5l3YuHp0Sd9TaUyVfp_9u3NkZboAZqsAxvM,5933
56
- gamesentenceminer-2.8.13.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
57
- gamesentenceminer-2.8.13.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
58
- gamesentenceminer-2.8.13.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
59
- gamesentenceminer-2.8.13.dist-info/RECORD,,
53
+ GameSentenceMiner/web/templates/utility.html,sha256=NUp4Yjs6_j7YeqsM2rcF0LzwS6nXSBUWJDl-k2E8BbM,16270
54
+ gamesentenceminer-2.8.15.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
55
+ gamesentenceminer-2.8.15.dist-info/METADATA,sha256=bLKHdjfoOke5LB1zD8IerMRtf_WvCAaAsSGtUxcg-Us,5933
56
+ gamesentenceminer-2.8.15.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
57
+ gamesentenceminer-2.8.15.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
58
+ gamesentenceminer-2.8.15.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
59
+ gamesentenceminer-2.8.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.1)
2
+ Generator: setuptools (80.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5