GameSentenceMiner 2.18.8__py3-none-any.whl → 2.18.9__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.
@@ -1,14 +1,18 @@
1
1
  import tkinter as tk
2
2
  from tkinter import scrolledtext
3
+ from tkinter import messagebox
3
4
  from PIL import Image, ImageTk
4
5
 
5
6
  import ttkbootstrap as ttk
6
- from GameSentenceMiner.util.configuration import get_config, logger, gsm_state
7
+ from GameSentenceMiner.util.configuration import get_config, logger, gsm_state, get_temporary_directory
7
8
  from GameSentenceMiner.util.audio_player import AudioPlayer
9
+ from GameSentenceMiner.util.gsm_utils import make_unique_file_name
8
10
 
9
11
  import platform
10
12
  import subprocess
11
13
  import os
14
+ import requests
15
+ from urllib.parse import quote
12
16
 
13
17
  class AnkiConfirmationDialog(tk.Toplevel):
14
18
  """
@@ -20,13 +24,18 @@ class AnkiConfirmationDialog(tk.Toplevel):
20
24
  self.screenshot_timestamp = screenshot_timestamp
21
25
  self.translation_text = None
22
26
  self.sentence_text = None
27
+ self.sentence = sentence # Store sentence text for TTS
23
28
 
24
29
  # Initialize screenshot_path here, will be updated by button if needed
25
- self.screenshot_path = screenshot_path
30
+ self.screenshot_path = screenshot_path
31
+ self.audio_path = audio_path # Store audio path so it can be updated
26
32
 
27
33
  # Audio player management
28
34
  self.audio_player = AudioPlayer(finished_callback=self._audio_finished)
29
35
  self.audio_button = None # Store reference to audio button
36
+ self.audio_path_label = None # Store reference to audio path label
37
+ self.tts_button = None # Store reference to TTS button
38
+ self.tts_status_label = None # Store reference to TTS status label
30
39
 
31
40
  self.title("Confirm Anki Card Details")
32
41
  self.result = None # This will store the user's choice
@@ -123,18 +132,36 @@ class AnkiConfirmationDialog(tk.Toplevel):
123
132
  # Audio Path
124
133
  if audio_path and os.path.isfile(audio_path):
125
134
  ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
126
- ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
135
+ self.audio_path_label = ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left")
136
+ self.audio_path_label.grid(row=row, column=1, sticky="w", padx=5, pady=2)
127
137
  if audio_path and os.path.isfile(audio_path):
128
138
  self.audio_button = ttk.Button(
129
- main_frame,
130
- text="▶",
131
- command=lambda: self._play_audio(audio_path),
139
+ main_frame,
140
+ text="▶",
141
+ command=lambda: self._play_audio(self.audio_path),
132
142
  bootstyle="outline-info",
133
143
  width=12
134
144
  )
135
145
  self.audio_button.grid(row=row, column=2, sticky="w", padx=5, pady=2)
136
146
 
137
147
  row += 1
148
+
149
+ # TTS Button - only show if TTS is enabled in config
150
+ if get_config().vad.use_tts_as_fallback and sentence:
151
+ self.tts_button = ttk.Button(
152
+ main_frame,
153
+ text="🔊 Generate TTS Audio",
154
+ command=self._generate_tts_audio,
155
+ bootstyle="info",
156
+ width=20
157
+ )
158
+ self.tts_button.grid(row=row, column=1, sticky="w", padx=5, pady=2)
159
+
160
+ # TTS Status Label
161
+ self.tts_status_label = ttk.Label(main_frame, text="", foreground="green")
162
+ self.tts_status_label.grid(row=row, column=2, sticky="w", padx=5, pady=2)
163
+
164
+ row += 1
138
165
 
139
166
  # Action Buttons
140
167
  button_frame = ttk.Frame(main_frame)
@@ -215,6 +242,72 @@ class AnkiConfirmationDialog(tk.Toplevel):
215
242
  else:
216
243
  self.audio_button.config(text="▶ Play Audio", bootstyle="outline-info")
217
244
 
245
+ def _generate_tts_audio(self):
246
+ """Generate TTS audio from the sentence text"""
247
+ try:
248
+ # Get the current sentence text from the widget
249
+ sentence_text = self.sentence_text.get("1.0", tk.END).strip()
250
+
251
+ if not sentence_text:
252
+ messagebox.showerror("TTS Error", "No sentence text available for TTS generation.")
253
+ return
254
+
255
+ # URL-encode the sentence text
256
+ encoded_text = quote(sentence_text)
257
+
258
+ # Build the TTS URL by replacing $s with the encoded text
259
+ tts_url = get_config().vad.tts_url.replace("$s", encoded_text)
260
+
261
+ logger.info(f"Fetching TTS audio from: {tts_url}")
262
+
263
+ # Fetch TTS audio from the URL
264
+ response = requests.get(tts_url, timeout=10)
265
+
266
+ if not response.ok:
267
+ error_msg = f"Failed to fetch TTS audio: HTTP {response.status_code}"
268
+ logger.error(error_msg)
269
+ messagebox.showerror("TTS Error", f"{error_msg}\n\nIs your TTS service running?")
270
+ return
271
+
272
+ # Save TTS audio to GSM temporary directory with game name
273
+ game_name = gsm_state.current_game if gsm_state.current_game else "tts"
274
+ filename = f"{game_name}_tts_audio.opus"
275
+ tts_audio_path = make_unique_file_name(
276
+ os.path.join(get_temporary_directory(), filename)
277
+ )
278
+ with open(tts_audio_path, 'wb') as f:
279
+ f.write(response.content)
280
+
281
+ logger.info(f"TTS audio saved to: {tts_audio_path}")
282
+
283
+ # Update the audio path
284
+ self.audio_path = tts_audio_path
285
+
286
+ # Update the audio path label
287
+ if self.audio_path_label:
288
+ self.audio_path_label.config(text=tts_audio_path)
289
+
290
+ # Update the audio button command to use the new path
291
+ if self.audio_button:
292
+ self.audio_button.config(command=lambda: self._play_audio(self.audio_path))
293
+
294
+ # Update status label to show success
295
+ if self.tts_status_label:
296
+ self.tts_status_label.config(text="✓ TTS Audio Generated", foreground="green")
297
+
298
+ except requests.exceptions.Timeout:
299
+ error_msg = "TTS request timed out. Please check if your TTS service is running."
300
+ logger.error(error_msg)
301
+ messagebox.showerror("TTS Error", error_msg)
302
+ except requests.exceptions.RequestException as e:
303
+ error_msg = f"Failed to connect to TTS service: {str(e)}"
304
+ logger.error(error_msg)
305
+ messagebox.showerror("TTS Error", f"{error_msg}\n\nPlease check your TTS URL configuration.")
306
+ except Exception as e:
307
+ error_msg = f"Unexpected error generating TTS: {str(e)}"
308
+ logger.error(error_msg)
309
+ messagebox.showerror("TTS Error", error_msg)
310
+
218
311
  def _cleanup_audio(self):
219
312
  """Clean up audio stream resources"""
220
313
  self.audio_player.cleanup()
@@ -106,10 +106,10 @@ class OverlayThread(threading.Thread):
106
106
  while True:
107
107
  if overlay_server_thread.has_clients():
108
108
  if get_config().overlay.periodic:
109
- await self.overlay_processor.find_box_and_send_to_overlay('')
109
+ await self.overlay_processor.find_box_and_send_to_overlay('', True)
110
110
  await asyncio.sleep(get_config().overlay.periodic_interval)
111
111
  elif self.first_time_run:
112
- await self.overlay_processor.find_box_and_send_to_overlay('')
112
+ await self.overlay_processor.find_box_and_send_to_overlay('', False)
113
113
  self.first_time_run = False
114
114
  else:
115
115
  await asyncio.sleep(3)
@@ -161,7 +161,7 @@ class OverlayProcessor:
161
161
  self.lens = None
162
162
  self.regex = None
163
163
 
164
- async def find_box_and_send_to_overlay(self, sentence_to_check: str = None):
164
+ async def find_box_and_send_to_overlay(self, sentence_to_check: str = None, check_against_last: bool = False):
165
165
  """
166
166
  Sends the detected text boxes to the overlay via WebSocket.
167
167
  Cancels any running OCR task before starting a new one.
@@ -175,7 +175,7 @@ class OverlayProcessor:
175
175
  logger.info("Previous OCR task was cancelled")
176
176
 
177
177
  # Start new task
178
- self.current_task = asyncio.create_task(self.find_box_for_sentence(sentence_to_check))
178
+ self.current_task = asyncio.create_task(self.find_box_for_sentence(sentence_to_check, check_against_last))
179
179
  try:
180
180
  await self.current_task
181
181
  except asyncio.CancelledError:
@@ -183,7 +183,7 @@ class OverlayProcessor:
183
183
  # logger.info(f"Sending {len(boxes)} boxes to overlay.")
184
184
  # await send_word_coordinates_to_overlay(boxes)
185
185
 
186
- async def find_box_for_sentence(self, sentence_to_check: str = None) -> List[Dict[str, Any]]:
186
+ async def find_box_for_sentence(self, sentence_to_check: str = None, check_against_last: bool = False) -> List[Dict[str, Any]]:
187
187
  """
188
188
  Public method to perform OCR and find text boxes for a given sentence.
189
189
 
@@ -191,7 +191,7 @@ class OverlayProcessor:
191
191
  error handling.
192
192
  """
193
193
  try:
194
- return await self._do_work(sentence_to_check)
194
+ return await self._do_work(sentence_to_check, check_against_last=check_against_last)
195
195
  except Exception as e:
196
196
  logger.error(f"Error during OCR processing: {e}", exc_info=True)
197
197
  return []
@@ -304,7 +304,7 @@ class OverlayProcessor:
304
304
 
305
305
  return composite_img
306
306
 
307
- async def _do_work(self, sentence_to_check: str = None) -> Tuple[List[Dict[str, Any]], int]:
307
+ async def _do_work(self, sentence_to_check: str = None, check_against_last: bool = False) -> Tuple[List[Dict[str, Any]], int]:
308
308
  """The main OCR workflow with cancellation support."""
309
309
  if not self.lens:
310
310
  logger.error("OCR engines are not initialized. Cannot perform OCR for Overlay.")
@@ -348,7 +348,7 @@ class OverlayProcessor:
348
348
  text_str = "".join([text for text in text if self.regex.match(text)])
349
349
 
350
350
  # RapidFuzz fuzzy match 90% to not send the same results repeatedly
351
- if self.last_oneocr_result:
351
+ if self.last_oneocr_result and check_against_last:
352
352
 
353
353
  score = fuzz.ratio(text_str, self.last_oneocr_result)
354
354
  if score >= 80:
@@ -403,7 +403,7 @@ class OverlayProcessor:
403
403
  text_str = "".join([text for text in text_list if self.regex.match(text)])
404
404
 
405
405
  # RapidFuzz fuzzy match 90% to not send the same results repeatedly
406
- if self.last_lens_result:
406
+ if self.last_lens_result and check_against_last:
407
407
  score = fuzz.ratio(text_str, self.last_lens_result)
408
408
  if score >= 80:
409
409
  logger.info("Google Lens results are similar to the last results (score: %d). Skipping overlay update.", score)
@@ -596,7 +596,7 @@ async def main_run_ocr():
596
596
  """
597
597
  overlay_processor = OverlayProcessor()
598
598
  while True:
599
- await overlay_processor.find_box_and_send_to_overlay('')
599
+ await overlay_processor.find_box_and_send_to_overlay('', False)
600
600
  await asyncio.sleep(10)
601
601
 
602
602
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.18.8
3
+ Version: 2.18.9
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -36,7 +36,7 @@ GameSentenceMiner/tools/furigana_filter_preview.py,sha256=BXv7FChPEJW_VeG5XYt6su
36
36
  GameSentenceMiner/tools/ss_selector.py,sha256=ob2oJdiYreDMMau7CvsglpnhZ1CDnJqop3lV54-PjRo,4782
37
37
  GameSentenceMiner/tools/window_transparency.py,sha256=GtbxbmZg0-UYPXhfHff-7IKZyY2DKe4B9GdyovfmpeM,8166
38
38
  GameSentenceMiner/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- GameSentenceMiner/ui/anki_confirmation.py,sha256=S3gN4hr2wZVazWr3F3xYybsaClpkugq3nwYKxIEYRLE,10840
39
+ GameSentenceMiner/ui/anki_confirmation.py,sha256=ohpWlPTvKn-_5_lpINdKZciR0k8RWRfnDrfuzyJgItc,15242
40
40
  GameSentenceMiner/ui/config_gui.py,sha256=4baqfL33oMshmqm903GZok32Y4JIEV-3K9gf5gxAJDU,152131
41
41
  GameSentenceMiner/ui/screenshot_selector.py,sha256=AKML87MpgYQeSuj1F10GngpNrn9qp06zLLzNRwrQWM8,8900
42
42
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -45,7 +45,7 @@ GameSentenceMiner/util/configuration.py,sha256=lwo73S3xnIMPq8lWSWM6N0pd08A4-Jvre
45
45
  GameSentenceMiner/util/db.py,sha256=1DjGjlwWnPefmQfzvMqqFPW0a0qeO-fIXE1YqKiok18,32000
46
46
  GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
47
47
  GameSentenceMiner/util/ffmpeg.py,sha256=cAzztfY36Xf2WvsJDjavoiMOvA9ac2GVdCrSB4LzHk4,29007
48
- GameSentenceMiner/util/get_overlay_coords.py,sha256=4V04RNVSIoiGrxRbYgzec2r29L8s7kmOjI_tuwfjLhI,24592
48
+ GameSentenceMiner/util/get_overlay_coords.py,sha256=MFl_JOjwzD0D0iZBPcq5Dgy32YPMKqRrugL0WsfMEu4,24819
49
49
  GameSentenceMiner/util/gsm_utils.py,sha256=mASECTmN10c2yPL4NEfLg0Y0YWwFso1i6r_hhJPR3MY,10974
50
50
  GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
51
51
  GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
@@ -125,9 +125,9 @@ GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic
125
125
  GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json,sha256=8wjnnaYQqmho6t5tMxrIAc03512A2tYhQh5dfsQnfAM,11372
126
126
  GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json,sha256=wRkqZNPzz6DT9OTPHpXwfqW96Qb96stCQNNgOL-ZdKk,17535
127
127
  GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- gamesentenceminer-2.18.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
129
- gamesentenceminer-2.18.8.dist-info/METADATA,sha256=Te9wJkpNUZGU2Vm4ALzEL3ygz-hDDrpMIQSD2QoFhho,7487
130
- gamesentenceminer-2.18.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
131
- gamesentenceminer-2.18.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
132
- gamesentenceminer-2.18.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
133
- gamesentenceminer-2.18.8.dist-info/RECORD,,
128
+ gamesentenceminer-2.18.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
129
+ gamesentenceminer-2.18.9.dist-info/METADATA,sha256=4CeXXcMgBMAwMH_61VDFQWr_xyt-5Fg4XDsQ2sfJA6s,7487
130
+ gamesentenceminer-2.18.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
131
+ gamesentenceminer-2.18.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
132
+ gamesentenceminer-2.18.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
133
+ gamesentenceminer-2.18.9.dist-info/RECORD,,