GameSentenceMiner 2.18.7__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
  """
@@ -18,13 +22,20 @@ class AnkiConfirmationDialog(tk.Toplevel):
18
22
  super().__init__(parent)
19
23
  self.config_app = config_app
20
24
  self.screenshot_timestamp = screenshot_timestamp
25
+ self.translation_text = None
26
+ self.sentence_text = None
27
+ self.sentence = sentence # Store sentence text for TTS
21
28
 
22
29
  # Initialize screenshot_path here, will be updated by button if needed
23
- 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
24
32
 
25
33
  # Audio player management
26
34
  self.audio_player = AudioPlayer(finished_callback=self._audio_finished)
27
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
28
39
 
29
40
  self.title("Confirm Anki Card Details")
30
41
  self.result = None # This will store the user's choice
@@ -121,18 +132,36 @@ class AnkiConfirmationDialog(tk.Toplevel):
121
132
  # Audio Path
122
133
  if audio_path and os.path.isfile(audio_path):
123
134
  ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
124
- 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)
125
137
  if audio_path and os.path.isfile(audio_path):
126
138
  self.audio_button = ttk.Button(
127
- main_frame,
128
- text="▶",
129
- command=lambda: self._play_audio(audio_path),
139
+ main_frame,
140
+ text="▶",
141
+ command=lambda: self._play_audio(self.audio_path),
130
142
  bootstyle="outline-info",
131
143
  width=12
132
144
  )
133
145
  self.audio_button.grid(row=row, column=2, sticky="w", padx=5, pady=2)
134
146
 
135
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
136
165
 
137
166
  # Action Buttons
138
167
  button_frame = ttk.Frame(main_frame)
@@ -213,6 +242,72 @@ class AnkiConfirmationDialog(tk.Toplevel):
213
242
  else:
214
243
  self.audio_button.config(text="▶ Play Audio", bootstyle="outline-info")
215
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
+
216
311
  def _cleanup_audio(self):
217
312
  """Clean up audio stream resources"""
218
313
  self.audio_player.cleanup()
@@ -226,13 +321,13 @@ class AnkiConfirmationDialog(tk.Toplevel):
226
321
  # Clean up audio before closing
227
322
  self._cleanup_audio()
228
323
  # The screenshot_path is now correctly updated if the user chose a new one
229
- self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
324
+ self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
230
325
  self.destroy()
231
326
 
232
327
  def _on_no_voice(self):
233
328
  # Clean up audio before closing
234
329
  self._cleanup_audio()
235
- self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
330
+ self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
236
331
  self.destroy()
237
332
 
238
333
  def _on_cancel(self):
@@ -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.7
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=fAPdZ_nfpSCuhLBim7jpHNXCnlODWwQs-c8qAS-brwU,10699
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.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
129
- gamesentenceminer-2.18.7.dist-info/METADATA,sha256=tSdKTLxo7GQEm-9Zea4mSZs_2Z1unk-1jPRF3BXEM_Q,7487
130
- gamesentenceminer-2.18.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
131
- gamesentenceminer-2.18.7.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
132
- gamesentenceminer-2.18.7.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
133
- gamesentenceminer-2.18.7.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,,