GameSentenceMiner 2.14.3__tar.gz → 2.14.5__tar.gz

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.
Files changed (82) hide show
  1. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ai/ai_prompting.py +26 -24
  2. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/anki.py +8 -8
  3. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/config_gui.py +111 -49
  4. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/locales/en_us.json +1 -1
  5. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/obs.py +42 -5
  6. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/gsm_ocr_config.py +8 -2
  7. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/owocr_area_selector.py +17 -0
  8. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/ocr.py +42 -9
  9. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/run.py +209 -26
  10. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/configuration.py +6 -0
  11. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/electron_config.py +2 -2
  12. gamesentenceminer-2.14.5/GameSentenceMiner/web/templates/index.html +50 -0
  13. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/texthooking_page.py +30 -0
  14. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/PKG-INFO +9 -4
  15. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/requires.txt +10 -3
  16. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/PKG-INFO +9 -4
  17. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/pyproject.toml +10 -5
  18. gamesentenceminer-2.14.3/GameSentenceMiner/web/templates/index.html +0 -50
  19. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/__init__.py +0 -0
  20. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ai/__init__.py +0 -0
  21. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/__init__.py +0 -0
  22. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon.png +0 -0
  23. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon128.png +0 -0
  24. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon256.png +0 -0
  25. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon32.png +0 -0
  26. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon512.png +0 -0
  27. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/icon64.png +0 -0
  28. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/assets/pickaxe.png +0 -0
  29. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/gametext.py +0 -0
  30. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/gsm.py +0 -0
  31. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/locales/ja_jp.json +0 -0
  32. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/locales/zh_cn.json +0 -0
  33. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/__init__.py +0 -0
  34. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  35. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  36. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  37. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  38. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  39. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  40. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  41. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  42. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/tools/__init__.py +0 -0
  43. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  44. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/tools/ss_selector.py +0 -0
  45. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/tools/window_transparency.py +0 -0
  46. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/__init__.py +0 -0
  47. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/communication/__init__.py +0 -0
  48. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/communication/send.py +0 -0
  49. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/communication/websocket.py +0 -0
  50. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/db.py +0 -0
  51. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  52. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  53. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  54. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  55. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/ffmpeg.py +0 -0
  56. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  57. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/gsm_utils.py +0 -0
  58. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/model.py +0 -0
  59. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/notification.py +0 -0
  60. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/util/text_log.py +0 -0
  61. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/vad.py +0 -0
  62. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/__init__.py +0 -0
  63. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/service.py +0 -0
  64. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/__init__.py +0 -0
  65. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  66. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  67. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/favicon.ico +0 -0
  68. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/favicon.svg +0 -0
  69. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  70. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/style.css +0 -0
  71. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  72. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  73. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/templates/__init__.py +0 -0
  74. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  75. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/web/templates/utility.html +0 -0
  76. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner/wip/__init___.py +0 -0
  77. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  78. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  79. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  80. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  81. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/LICENSE +0 -0
  82. {gamesentenceminer-2.14.3 → gamesentenceminer-2.14.5}/setup.cfg +0 -0
@@ -24,12 +24,13 @@ TRANSLATION_PROMPT = f"""
24
24
  **Professional Game Localization Task**
25
25
 
26
26
  **Task Directive:**
27
- Translate ONLY the single line of game dialogue specified below into natural-sounding, context-aware {get_config().general.get_native_language_name()}. The translation must preserve the original tone and intent of the character.
27
+ Translate ONLY the provided line of game dialogue specified below into natural-sounding, context-aware {get_config().general.get_native_language_name()}. The translation must preserve the original tone and intent of the source.
28
28
 
29
29
  **Output Requirements:**
30
30
  - Provide only the single, best {get_config().general.get_native_language_name()} translation.
31
31
  - Use expletives if they are natural for the context and enhance the translation's impact, but do not over-exaggerate.
32
- - Preserve all HTML tags present in the original text on their corresponding words in the translation.
32
+ - Carryover all HTML tags present in the original text to HTML tags surrounding their corresponding words in the translation. DO NOT CONVERT TO MARKDOWN.
33
+ - If there are no HTML tags present in the original text, do not add any in the translation whatsoever.
33
34
  - Do not include notes, alternatives, explanations, or any other surrounding text. Absolutely nothing but the translated line.
34
35
 
35
36
  **Line to Translate:**
@@ -77,11 +78,11 @@ class AIManager(ABC):
77
78
  self.logger = logger
78
79
 
79
80
  @abstractmethod
80
- def process(self, lines: List[GameLine], sentence: str, current_line_index: int, game_title: str = "") -> str:
81
+ def process(self, lines: List[GameLine], sentence: str, current_line_index: int, game_title: str = "", custom_prompt=None) -> str:
81
82
  pass
82
83
 
83
84
  @abstractmethod
84
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
85
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str, custom_prompt=None) -> str:
85
86
  if get_config().ai.dialogue_context_length != 0:
86
87
  if get_config().ai.dialogue_context_length == -1:
87
88
  start_index = 0
@@ -104,8 +105,9 @@ class AIManager(ABC):
104
105
  """
105
106
  else:
106
107
  dialogue_context = "No dialogue context available."
107
-
108
- if get_config().ai.use_canned_translation_prompt:
108
+ if custom_prompt:
109
+ prompt_to_use = custom_prompt
110
+ elif get_config().ai.use_canned_translation_prompt:
109
111
  prompt_to_use = TRANSLATION_PROMPT
110
112
  elif get_config().ai.use_canned_context_prompt:
111
113
  prompt_to_use = CONTEXT_PROMPT
@@ -121,7 +123,7 @@ class AIManager(ABC):
121
123
 
122
124
  {sentence}
123
125
  """)
124
- return full_prompt
126
+ return textwrap.dedent(full_prompt)
125
127
 
126
128
 
127
129
  class OpenAIManager(AIManager):
@@ -143,11 +145,11 @@ class OpenAIManager(AIManager):
143
145
  self.openai = None
144
146
  self.model_name = None
145
147
 
146
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
147
- prompt = super()._build_prompt(lines, sentence, current_line, game_title)
148
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str, custom_prompt=None) -> str:
149
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
148
150
  return prompt
149
-
150
- def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
151
+
152
+ def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", custom_prompt=None) -> str:
151
153
  if self.client is None:
152
154
  return "Processing failed: OpenAI client not initialized."
153
155
 
@@ -156,7 +158,7 @@ class OpenAIManager(AIManager):
156
158
  return "Invalid input."
157
159
 
158
160
  try:
159
- prompt = self._build_prompt(lines, sentence, current_line, game_title)
161
+ prompt = self._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
160
162
  self.logger.debug(f"Generated prompt:\n{prompt}")
161
163
  response = self.client.chat.completions.create(
162
164
  model=self.model_name,
@@ -166,7 +168,7 @@ class OpenAIManager(AIManager):
166
168
  ],
167
169
  temperature=0.3,
168
170
  max_tokens=4096,
169
- top_p=1,
171
+ top_p=0.9,
170
172
  n=1,
171
173
  stop=None,
172
174
  )
@@ -196,7 +198,7 @@ class GeminiAI(AIManager):
196
198
  self.generation_config = types.GenerateContentConfig(
197
199
  temperature=0.5,
198
200
  max_output_tokens=1024,
199
- top_p=1,
201
+ top_p=0.9,
200
202
  stop_sequences=None,
201
203
  safety_settings=[
202
204
  types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
@@ -214,11 +216,11 @@ class GeminiAI(AIManager):
214
216
  self.logger.error(f"Failed to initialize Gemini API: {e}")
215
217
  self.model_name = None
216
218
 
217
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
218
- prompt = super()._build_prompt(lines, sentence, current_line, game_title)
219
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str, custom_prompt=None) -> str:
220
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
219
221
  return prompt
220
222
 
221
- def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
223
+ def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", custom_prompt=None) -> str:
222
224
  if self.model_name is None:
223
225
  return "Processing failed: AI model not initialized."
224
226
 
@@ -227,7 +229,7 @@ class GeminiAI(AIManager):
227
229
  return "Invalid input."
228
230
 
229
231
  try:
230
- prompt = self._build_prompt(lines, sentence, current_line, game_title)
232
+ prompt = self._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
231
233
  contents = [
232
234
  types.Content(
233
235
  role="user",
@@ -262,11 +264,11 @@ class GroqAI(AIManager):
262
264
  self.logger.error(f"Failed to initialize Groq client: {e}")
263
265
  self.client = None
264
266
 
265
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
266
- prompt = super()._build_prompt(lines, sentence, current_line, game_title)
267
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str, custom_prompt=None) -> str:
268
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
267
269
  return prompt
268
270
 
269
- def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
271
+ def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", custom_prompt=None) -> str:
270
272
  if self.client is None:
271
273
  return "Processing failed: Groq client not initialized."
272
274
 
@@ -275,7 +277,7 @@ class GroqAI(AIManager):
275
277
  return "Invalid input."
276
278
 
277
279
  try:
278
- prompt = self._build_prompt(lines, sentence, current_line, game_title)
280
+ prompt = self._build_prompt(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
279
281
  self.logger.debug(f"Generated prompt:\n{prompt}")
280
282
  completion = self.client.chat.completions.create(
281
283
  model=self.model_name,
@@ -297,7 +299,7 @@ ai_managers: dict[str, AIManager] = {}
297
299
  ai_manager: AIManager | None = None
298
300
  current_ai_config: Ai | None = None
299
301
 
300
- def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", force_refresh: bool = False) -> str:
302
+ def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", force_refresh: bool = False, custom_prompt=None) -> str:
301
303
  global ai_manager, current_ai_config
302
304
  try:
303
305
  is_local_provider = get_config().ai.provider == AIType.OPENAI.value
@@ -334,7 +336,7 @@ def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: Gam
334
336
  if not ai_manager:
335
337
  logger.error("AI is enabled but the AI Manager did not initialize. Check your AI Config IN GSM.")
336
338
  return ""
337
- return ai_manager.process(lines, sentence, current_line, game_title)
339
+ return ai_manager.process(lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
338
340
  except Exception as e:
339
341
  logger.error("Error caught while trying to get AI prompt result. Check logs for more details.")
340
342
  logger.debug(e, exc_info=True)
@@ -134,14 +134,11 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
134
134
 
135
135
  if note and 'fields' in note and get_config().ai.enabled:
136
136
  sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
137
- if not selected_lines and game_line.TL:
138
- logger.info("Using TL from texthooker for AI Prompt Result")
139
- translation = game_line.TL
140
- else:
141
- sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
142
- get_config().anki.sentence_field)
143
- translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate,
144
- game_line, get_current_game())
137
+ sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
138
+ get_config().anki.sentence_field)
139
+ translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate,
140
+ game_line, get_current_game())
141
+ game_line.TL = translation
145
142
  logger.info(f"AI prompt Result: {translation}")
146
143
  note['fields'][get_config().ai.anki_field] = translation
147
144
 
@@ -502,3 +499,6 @@ def start_monitoring_anki():
502
499
  obs_thread.daemon = True
503
500
  obs_thread.start()
504
501
 
502
+
503
+ if __name__ == "__main__":
504
+ print(invoke("getIntervals", cards=["1754694986036"]))
@@ -110,6 +110,30 @@ class HoverInfoLabelWidget:
110
110
  if self.tooltip:
111
111
  self.tooltip.destroy()
112
112
  self.tooltip = None
113
+
114
+ class HoverInfoEntryWidget:
115
+ def __init__(self, parent, text, row, column, padx=5, pady=2, textvariable=None):
116
+ self.entry = ttk.Entry(parent, textvariable=textvariable)
117
+ self.entry.grid(row=row, column=column, padx=padx, pady=pady)
118
+ self.entry.bind("<Enter>", lambda e: self.show_info_box(text))
119
+ self.entry.bind("<Leave>", lambda e: self.hide_info_box())
120
+ self.tooltip = None
121
+
122
+ def show_info_box(self, text):
123
+ x, y, _, _ = self.entry.bbox("insert")
124
+ x += self.entry.winfo_rootx() + 25
125
+ y += self.entry.winfo_rooty() + 20
126
+ self.tooltip = tk.Toplevel(self.entry)
127
+ self.tooltip.wm_overrideredirect(True)
128
+ self.tooltip.wm_geometry(f"+{x}+{y}")
129
+ label = ttk.Label(self.tooltip, text=text, relief="solid", borderwidth=1,
130
+ font=("tahoma", "12", "normal"))
131
+ label.pack(ipadx=1)
132
+
133
+ def hide_info_box(self):
134
+ if self.tooltip:
135
+ self.tooltip.destroy()
136
+ self.tooltip = None
113
137
 
114
138
 
115
139
  class ResetToDefaultButton(ttk.Button):
@@ -673,7 +697,8 @@ class ConfigApp:
673
697
  self.profiles_tab = None
674
698
  self.ai_tab = None
675
699
  self.advanced_tab = None
676
- self.wip_tab = None
700
+ self.overlay_tab = None
701
+ # self.wip_tab = None
677
702
 
678
703
  self.create_vars()
679
704
  self.create_tabs()
@@ -830,6 +855,7 @@ class ConfigApp:
830
855
 
831
856
  # --- General Settings ---
832
857
  general_i18n = self.i18n.get('tabs', {}).get('general', {})
858
+
833
859
  input_frame = ttk.Frame(required_settings_frame)
834
860
  input_frame.grid(row=self.current_row, column=0, columnspan=4, sticky='W', pady=2)
835
861
 
@@ -854,7 +880,7 @@ class ConfigApp:
854
880
  HoverInfoLabelWidget(required_settings_frame, text=locale_i18n.get('label', '...'),
855
881
  tooltip=locale_i18n.get('tooltip', '...'), row=self.current_row, column=0)
856
882
  locale_combobox_simple = ttk.Combobox(required_settings_frame, textvariable=self.locale_value, values=[Locale.English.name, Locale.日本語.name, Locale.中文.name], state="readonly")
857
- locale_combobox_simple.grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
883
+ locale_combobox_simple.grid(row=self.current_row, column=1, columnspan=2, sticky='EW', pady=2)
858
884
  locale_combobox_simple.bind("<<ComboboxSelected>>", lambda e: self.change_locale())
859
885
  self.current_row += 1
860
886
 
@@ -878,9 +904,9 @@ class ConfigApp:
878
904
  ttk.Entry(required_settings_frame, textvariable=self.sentence_field_value).grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
879
905
  self.current_row += 1
880
906
 
881
- audio_i18n = anki_i18n.get('sentence_audio_field', {})
882
- HoverInfoLabelWidget(required_settings_frame, text=audio_i18n.get('label', '...'),
883
- tooltip=audio_i18n.get('tooltip', '...'), row=self.current_row, column=0)
907
+ sentence_audio_i18n = anki_i18n.get('sentence_audio_field', {})
908
+ HoverInfoLabelWidget(required_settings_frame, text=sentence_audio_i18n.get('label', '...'),
909
+ tooltip=sentence_audio_i18n.get('tooltip', '...'), row=self.current_row, column=0)
884
910
  ttk.Entry(required_settings_frame, textvariable=self.sentence_audio_field_value).grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
885
911
  self.current_row += 1
886
912
 
@@ -912,6 +938,45 @@ class ConfigApp:
912
938
  tooltip=vad_end_offset_i18n.get('tooltip', '...'), row=self.current_row, column=0)
913
939
  ttk.Entry(required_settings_frame, textvariable=self.end_offset_value).grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
914
940
  self.current_row += 1
941
+
942
+ splice_i18n = vad_i18n.get('cut_and_splice', {})
943
+ HoverInfoLabelWidget(required_settings_frame, text=splice_i18n.get('label', '...'),
944
+ tooltip=splice_i18n.get('tooltip', '...'),
945
+ row=self.current_row, column=0)
946
+ ttk.Checkbutton(required_settings_frame, variable=self.cut_and_splice_segments_value, bootstyle="round-toggle").grid(
947
+ row=self.current_row, column=1, sticky='W', pady=2)
948
+
949
+ padding_i18n = vad_i18n.get('splice_padding', {})
950
+ HoverInfoEntryWidget(required_settings_frame, text=padding_i18n.get('tooltip', '...'),
951
+ row=self.current_row, column=2, textvariable=self.splice_padding_value)
952
+
953
+ self.current_row += 1
954
+
955
+ # Ocen Audio
956
+
957
+ ext_tool_i18n = audio_tab_i18n.get('external_tool', {})
958
+ HoverInfoLabelWidget(required_settings_frame, text=ext_tool_i18n.get('label', '...'),
959
+ tooltip=ext_tool_i18n.get('tooltip', '...'),
960
+ foreground="green", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
961
+ self.external_tool_entry = ttk.Entry(required_settings_frame, textvariable=self.external_tool_value)
962
+ self.external_tool_entry.grid(row=self.current_row, column=1, sticky='EW', pady=2)
963
+
964
+ ttk.Button(required_settings_frame, text=audio_tab_i18n.get('install_ocenaudio_button', 'Install Ocenaudio'), command=self.download_and_install_ocen,
965
+ bootstyle="info").grid(row=self.current_row, column=2, pady=5)
966
+ self.current_row += 1
967
+
968
+ # ext_tool_enabled_i18n = audio_tab_i18n.get('external_tool_enabled', {})
969
+ # ttk.Checkbutton(required_settings_frame, variable=self.external_tool_enabled_value, bootstyle="round-toggle").grid(
970
+ # row=self.current_row, column=3, sticky='W', padx=10, pady=5)
971
+ # self.current_row += 1
972
+
973
+ # Anki Media Collection
974
+
975
+ # anki_media_collection_i18n = audio_tab_i18n.get('anki_media_collection', {})
976
+ # HoverInfoLabelWidget(required_settings_frame, text=anki_media_collection_i18n.get('label', '...'),
977
+ # tooltip=anki_media_collection_i18n.get('tooltip', '...'), row=self.current_row, column=0)
978
+ # ttk.Entry(required_settings_frame, textvariable=self.anki_media_collection_value).grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
979
+ # self.current_row += 1
915
980
 
916
981
  # --- Features Settings ---
917
982
  features_i18n = self.i18n.get('tabs', {}).get('features', {})
@@ -1554,9 +1619,9 @@ class ConfigApp:
1554
1619
 
1555
1620
  ttk.Button(audio_frame, text=audio_i18n.get('install_ocenaudio_button', 'Install Ocenaudio'), command=self.download_and_install_ocen,
1556
1621
  bootstyle="info").grid(row=self.current_row, column=0, pady=5)
1557
- ttk.Button(audio_frame, text=audio_i18n.get('get_anki_media_button', 'Get Anki Media Collection'),
1558
- command=self.set_default_anki_media_collection, bootstyle="info").grid(row=self.current_row,
1559
- column=1, pady=5)
1622
+ # ttk.Button(audio_frame, text=audio_i18n.get('get_anki_media_button', 'Get Anki Media Collection'),
1623
+ # command=self.set_default_anki_media_collection, bootstyle="info").grid(row=self.current_row,
1624
+ # column=1, pady=5)
1560
1625
  self.current_row += 1
1561
1626
 
1562
1627
  self.add_reset_button(audio_frame, "audio", self.current_row, 0, self.create_audio_tab)
@@ -2125,48 +2190,24 @@ class ConfigApp:
2125
2190
  wip_frame = self.wip_tab
2126
2191
  wip_i18n = self.i18n.get('tabs', {}).get('wip', {})
2127
2192
  try:
2128
- ttk.Label(wip_frame, text=wip_i18n.get('warning_experimental', '...'),
2129
- foreground="red", font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2,
2130
- sticky='W', pady=5)
2131
- self.current_row += 1
2132
-
2133
- ttk.Label(wip_frame, text=wip_i18n.get('warning_overlay_deps', '...'),
2134
- foreground="red", font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2,
2135
- sticky='W', pady=5)
2136
- self.current_row += 1
2137
-
2138
- overlay_port_i18n = wip_i18n.get('overlay_port', {})
2139
- HoverInfoLabelWidget(wip_frame, text=overlay_port_i18n.get('label', '...'),
2140
- tooltip=overlay_port_i18n.get('tooltip', '...'),
2141
- row=self.current_row, column=0)
2142
- ttk.Entry(wip_frame, textvariable=self.overlay_websocket_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2143
- self.current_row += 1
2144
-
2145
- overlay_send_i18n = wip_i18n.get('overlay_send', {})
2146
- HoverInfoLabelWidget(wip_frame, text=overlay_send_i18n.get('label', '...'),
2147
- tooltip=overlay_send_i18n.get('tooltip', '...'),
2148
- row=self.current_row, column=0)
2149
- ttk.Checkbutton(wip_frame, variable=self.overlay_websocket_send_value, bootstyle="round-toggle").grid(
2150
- row=self.current_row, column=1, sticky='W', pady=2)
2151
- self.current_row += 1
2152
-
2153
- monitor_i18n = wip_i18n.get('monitor_capture', {})
2154
- HoverInfoLabelWidget(wip_frame, text=monitor_i18n.get('label', '...'),
2155
- tooltip=monitor_i18n.get('tooltip', '...'),
2156
- row=self.current_row, column=0)
2157
- self.monitor_to_capture = ttk.Combobox(wip_frame, values=self.monitors, state="readonly")
2193
+ pass
2194
+ # from GameSentenceMiner.util.controller import ControllerInput, ControllerInputManager
2195
+ # HoverInfoLabelWidget(wip_frame, text=wip_i18n.get('note', 'This tab is a work in progress...'),
2196
+ # tooltip=wip_i18n.get('tooltip', '...'), foreground="blue", font=("Helvetica", 10, "bold"),
2197
+ # row=self.current_row, column=0, columnspan=2)
2198
+ # self.current_row += 1
2199
+
2200
+ # # Controller OCR Input
2201
+ # controller_ocr_input_i18n = wip_i18n.get('controller_ocr_input', {})
2202
+ # HoverInfoLabelWidget(wip_frame, text=controller_ocr_input_i18n.get('label', 'Controller OCR Input:'), tooltip=controller_ocr_input_i18n.get('tooltip', '...'),
2203
+ # row=self.current_row, column=0)
2204
+ # self.controller_ocr_input_value = tk.StringVar(value=getattr(self.settings.wip, 'controller_ocr_input', ''))
2205
+ # self.controller_hotkey_entry = ttk.Entry(wip_frame, textvariable=self.controller_ocr_input_value, width=50)
2206
+ # self.controller_hotkey_entry.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2158
2207
 
2159
- if self.monitors:
2160
- # Ensure the index is valid
2161
- monitor_index = self.settings.overlay.monitor_to_capture
2162
- if 0 <= monitor_index < len(self.monitors):
2163
- self.monitor_to_capture.current(monitor_index)
2164
- else:
2165
- self.monitor_to_capture.current(0)
2166
- else:
2167
- self.monitor_to_capture.set(monitor_i18n.get('not_detected', "OwOCR Not Detected"))
2168
- self.monitor_to_capture.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2169
- self.current_row += 1
2208
+ # listen_for_input_button = ttk.Button(wip_frame, text="Listen for Input", command=lambda: self.listen_for_controller_input())
2209
+ # listen_for_input_button.grid(row=self.current_row, column=2, sticky='EW', pady=2)
2210
+ # self.current_row += 1
2170
2211
 
2171
2212
  except Exception as e:
2172
2213
  logger.error(f"Error setting up wip tab to capture: {e}")
@@ -2178,6 +2219,27 @@ class ConfigApp:
2178
2219
  for row in range(self.current_row): wip_frame.grid_rowconfigure(row, minsize=30)
2179
2220
 
2180
2221
  return wip_frame
2222
+
2223
+ # def listen_for_controller_input(self):
2224
+ # from GameSentenceMiner.util.controller import ControllerInput, ControllerInputManager
2225
+ # def listen_for_controller_thread():
2226
+ # controller = ControllerInputManager()
2227
+ # controller.start()
2228
+ # start_time = time.time()
2229
+ # while time.time() - start_time < 10:
2230
+ # try:
2231
+ # event = controller.event_queue.get(timeout=1)
2232
+ # input = ''
2233
+ # for key in event:
2234
+ # input += key.readable_name + '+'
2235
+ # input = input[:-1] # Remove trailing '+'
2236
+ # self.controller_hotkey_entry.delete(0, tk.END)
2237
+ # self.controller_hotkey_entry.insert(0, input)
2238
+ # except Exception:
2239
+ # continue
2240
+ # controller.stop()
2241
+ # listen_thread = threading.Thread(target=listen_for_controller_thread)
2242
+ # listen_thread.start()
2181
2243
 
2182
2244
  def on_profile_change(self, event):
2183
2245
  self.save_settings(profile_change=True)
@@ -250,7 +250,7 @@
250
250
  "tooltip": "Beginning offset after VAD Trim, Only active if \"Trim Beginning\" is ON. Negative values = more time at the beginning"
251
251
  },
252
252
  "cut_and_splice": {
253
- "label": "Cut and Splice Segments:",
253
+ "label": "Cut and Splice Voice Segments:",
254
254
  "tooltip": "Cut Detected Voice Segments and Paste them back together. More Padding = More Space between voicelines."
255
255
  },
256
256
  "splice_padding": {
@@ -12,6 +12,8 @@ import obsws_python as obs
12
12
  from GameSentenceMiner.util import configuration
13
13
  from GameSentenceMiner.util.configuration import *
14
14
  from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name
15
+ import tkinter as tk
16
+ from tkinter import messagebox
15
17
 
16
18
  client: obs.ReqClient = None
17
19
  event_client: obs.EventClient = None
@@ -26,10 +28,13 @@ class OBSConnectionManager(threading.Thread):
26
28
  super().__init__()
27
29
  self.daemon = True
28
30
  self.running = True
31
+ self.check_connection_interval = 1
32
+ self.said_no_to_replay_buffer = False
33
+ self.counter = 0
29
34
 
30
35
  def run(self):
31
36
  while self.running:
32
- time.sleep(1)
37
+ time.sleep(self.check_connection_interval)
33
38
  try:
34
39
  if not connecting:
35
40
  client.get_version()
@@ -37,9 +42,39 @@ class OBSConnectionManager(threading.Thread):
37
42
  logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
38
43
  gsm_status.obs_connected = False
39
44
  asyncio.run(connect_to_obs())
45
+ if self.counter % 5 == 0:
46
+ replay_buffer_status = get_replay_buffer_status()
47
+ if replay_buffer_status and self.said_no_to_replay_buffer:
48
+ self.said_no_to_replay_buffer = False
49
+ self.counter = 0
50
+ if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
51
+ self.check_output()
52
+ self.counter += 1
40
53
 
41
54
  def stop(self):
42
55
  self.running = False
56
+
57
+ def check_output(self):
58
+ img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
59
+ extrema = img.getextrema()
60
+ if isinstance(extrema[0], tuple):
61
+ is_empty = all(e[0] == e[1] for e in extrema)
62
+ else:
63
+ is_empty = extrema[0] == extrema[1]
64
+ if is_empty:
65
+ logger.info("Image is totally empty (all pixels the same), sleeping.")
66
+ else:
67
+ root = tk.Tk()
68
+ root.attributes('-topmost', True)
69
+ root.withdraw()
70
+ root.deiconify()
71
+ result = messagebox.askyesno("GSM - Replay Buffer", "The replay buffer is not running, but there seems to be output in OBS. Do you want to start it? (If you click 'No', you won't be asked until you either restart GSM or start/stop replay buffer manually.)")
72
+ root.destroy()
73
+ if not result:
74
+ self.said_no_to_replay_buffer = True
75
+ self.counter = 0
76
+ return
77
+ start_replay_buffer()
43
78
 
44
79
  def get_obs_path():
45
80
  return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
@@ -248,9 +283,9 @@ def toggle_replay_buffer():
248
283
 
249
284
  def start_replay_buffer():
250
285
  try:
251
- status = get_replay_buffer_status()
252
- if status:
253
- client.start_replay_buffer()
286
+ response = client.start_replay_buffer()
287
+ if response and response.ok:
288
+ logger.info("Replay buffer started.")
254
289
  except Exception as e:
255
290
  logger.error(f"Error starting replay buffer: {e}")
256
291
 
@@ -263,7 +298,9 @@ def get_replay_buffer_status():
263
298
 
264
299
  def stop_replay_buffer():
265
300
  try:
266
- client.stop_replay_buffer()
301
+ response = client.stop_replay_buffer()
302
+ if response and response.ok:
303
+ logger.info("Replay buffer stopped.")
267
304
  except Exception as e:
268
305
  logger.warning(f"Error stopping replay buffer: {e}")
269
306
 
@@ -95,7 +95,7 @@ class OCRConfig:
95
95
  ]
96
96
 
97
97
  def has_config_changed(current_config: OCRConfig) -> bool:
98
- new_config = get_scene_ocr_config(use_window_as_config=get_ocr_use_window_for_config(), window=current_config.window)
98
+ new_config = get_scene_ocr_config(use_window_as_config=get_ocr_use_window_for_config(), window=current_config.window, refresh=True)
99
99
  if new_config.rectangles != current_config.rectangles:
100
100
  logger.info("OCR config has changed.")
101
101
  return True
@@ -139,8 +139,13 @@ def set_dpi_awareness():
139
139
  import ctypes
140
140
  per_monitor_awareness = 2
141
141
  ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
142
+
143
+ scene_ocr_config = None
142
144
 
143
- def get_scene_ocr_config(use_window_as_config=False, window=""):
145
+ def get_scene_ocr_config(use_window_as_config=False, window="", refresh=False) -> OCRConfig | None:
146
+ global scene_ocr_config
147
+ if scene_ocr_config and not refresh:
148
+ return scene_ocr_config
144
149
  path = get_scene_ocr_config_path(use_window_as_config, window)
145
150
  if not os.path.exists(path):
146
151
  return None
@@ -148,6 +153,7 @@ def get_scene_ocr_config(use_window_as_config=False, window=""):
148
153
  from json import load
149
154
  data = load(f)
150
155
  ocr_config = OCRConfig.from_dict(data)
156
+ scene_ocr_config = ocr_config
151
157
  return ocr_config
152
158
 
153
159
  def get_scene_ocr_config_path(use_window_as_config=False, window=""):
@@ -368,6 +368,23 @@ class ScreenSelector:
368
368
  # Lower the rectangle so it's behind the text
369
369
  canvas.tag_lower(self.instructions_rect, self.instructions_overlay)
370
370
 
371
+ # Add hover effect: make rectangle transparent on mouse over
372
+ def on_motion(event):
373
+ # Check if mouse is over the rectangle
374
+ x, y = event.x, event.y
375
+ rect_bbox = canvas.bbox(self.instructions_rect)
376
+ if rect_bbox and rect_bbox[0] <= x <= rect_bbox[2] and rect_bbox[1] <= y <= y <= rect_bbox[3]:
377
+ # Set fill to more transparent using denser stipple
378
+ canvas.itemconfigure(self.instructions_rect, fill='#2B2B2B', stipple='gray12')
379
+ # Make text more transparent by changing its color to a lighter gray
380
+ canvas.itemconfigure(self.instructions_overlay, fill='#CCCCCC')
381
+ else:
382
+ # Restore solid fill and opaque text
383
+ canvas.itemconfigure(self.instructions_rect, fill='#2B2B2B', stipple='')
384
+ canvas.itemconfigure(self.instructions_overlay, fill='white')
385
+
386
+ canvas.bind('<Motion>', on_motion)
387
+
371
388
 
372
389
  def toggle_instructions(self, event=None):
373
390
  canvas = event.widget.winfo_toplevel().winfo_children()[0]