GameSentenceMiner 2.14.4__py3-none-any.whl → 2.14.6__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.
@@ -24,13 +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
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
- - Maintain New Line Characters.
33
+ - If there are no HTML tags present in the original text, do not add any in the translation whatsoever.
34
34
  - Do not include notes, alternatives, explanations, or any other surrounding text. Absolutely nothing but the translated line.
35
35
 
36
36
  **Line to Translate:**
@@ -78,11 +78,11 @@ class AIManager(ABC):
78
78
  self.logger = logger
79
79
 
80
80
  @abstractmethod
81
- 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:
82
82
  pass
83
83
 
84
84
  @abstractmethod
85
- 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:
86
86
  if get_config().ai.dialogue_context_length != 0:
87
87
  if get_config().ai.dialogue_context_length == -1:
88
88
  start_index = 0
@@ -105,8 +105,9 @@ class AIManager(ABC):
105
105
  """
106
106
  else:
107
107
  dialogue_context = "No dialogue context available."
108
-
109
- 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:
110
111
  prompt_to_use = TRANSLATION_PROMPT
111
112
  elif get_config().ai.use_canned_context_prompt:
112
113
  prompt_to_use = CONTEXT_PROMPT
@@ -122,7 +123,7 @@ class AIManager(ABC):
122
123
 
123
124
  {sentence}
124
125
  """)
125
- return full_prompt
126
+ return textwrap.dedent(full_prompt)
126
127
 
127
128
 
128
129
  class OpenAIManager(AIManager):
@@ -144,11 +145,11 @@ class OpenAIManager(AIManager):
144
145
  self.openai = None
145
146
  self.model_name = None
146
147
 
147
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
148
- 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)
149
150
  return prompt
150
-
151
- 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:
152
153
  if self.client is None:
153
154
  return "Processing failed: OpenAI client not initialized."
154
155
 
@@ -157,7 +158,7 @@ class OpenAIManager(AIManager):
157
158
  return "Invalid input."
158
159
 
159
160
  try:
160
- 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)
161
162
  self.logger.debug(f"Generated prompt:\n{prompt}")
162
163
  response = self.client.chat.completions.create(
163
164
  model=self.model_name,
@@ -215,11 +216,11 @@ class GeminiAI(AIManager):
215
216
  self.logger.error(f"Failed to initialize Gemini API: {e}")
216
217
  self.model_name = None
217
218
 
218
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
219
- 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)
220
221
  return prompt
221
222
 
222
- 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:
223
224
  if self.model_name is None:
224
225
  return "Processing failed: AI model not initialized."
225
226
 
@@ -228,7 +229,7 @@ class GeminiAI(AIManager):
228
229
  return "Invalid input."
229
230
 
230
231
  try:
231
- 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)
232
233
  contents = [
233
234
  types.Content(
234
235
  role="user",
@@ -263,11 +264,11 @@ class GroqAI(AIManager):
263
264
  self.logger.error(f"Failed to initialize Groq client: {e}")
264
265
  self.client = None
265
266
 
266
- def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
267
- 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)
268
269
  return prompt
269
270
 
270
- 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:
271
272
  if self.client is None:
272
273
  return "Processing failed: Groq client not initialized."
273
274
 
@@ -276,7 +277,7 @@ class GroqAI(AIManager):
276
277
  return "Invalid input."
277
278
 
278
279
  try:
279
- 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)
280
281
  self.logger.debug(f"Generated prompt:\n{prompt}")
281
282
  completion = self.client.chat.completions.create(
282
283
  model=self.model_name,
@@ -298,7 +299,7 @@ ai_managers: dict[str, AIManager] = {}
298
299
  ai_manager: AIManager | None = None
299
300
  current_ai_config: Ai | None = None
300
301
 
301
- def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", force_refresh: bool = False, start_index = -1, end_index = -1) -> 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:
302
303
  global ai_manager, current_ai_config
303
304
  try:
304
305
  is_local_provider = get_config().ai.provider == AIType.OPENAI.value
@@ -335,7 +336,7 @@ def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: Gam
335
336
  if not ai_manager:
336
337
  logger.error("AI is enabled but the AI Manager did not initialize. Check your AI Config IN GSM.")
337
338
  return ""
338
- 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)
339
340
  except Exception as e:
340
341
  logger.error("Error caught while trying to get AI prompt result. Check logs for more details.")
341
342
  logger.debug(e, exc_info=True)
GameSentenceMiner/anki.py CHANGED
@@ -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": {
GameSentenceMiner/obs.py CHANGED
@@ -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=""):
@@ -378,8 +378,16 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
378
378
  previous_orig_text = orig_text_string
379
379
  previous_ocr1_result = previous_text
380
380
  if crop_coords and get_ocr_optimize_second_scan():
381
+ x1, y1, x2, y2 = crop_coords
382
+ x1 = max(0, min(x1, img.width))
383
+ y1 = max(0, min(y1, img.height))
384
+ x2 = max(x1, min(x2, img.width))
385
+ y2 = max(y1, min(y2, img.height))
381
386
  previous_img_local.save(os.path.join(get_temporary_directory(), "pre_oneocrcrop.png"))
382
- previous_img_local = previous_img_local.crop(crop_coords)
387
+ try:
388
+ previous_img_local = previous_img_local.crop((x1, y1, x2, y2))
389
+ except ValueError:
390
+ logger.warning("Error cropping image, using original image")
383
391
  second_ocr_queue.put((previous_text, stable_time, previous_img_local, filtering, pre_crop_image))
384
392
  # threading.Thread(target=do_second_ocr, args=(previous_text, stable_time, previous_img_local, filtering), daemon=True).start()
385
393
  previous_img = None
@@ -276,8 +276,10 @@ class GoogleLens:
276
276
  available = False
277
277
 
278
278
  def __init__(self, lang='ja'):
279
+ import regex
279
280
  self.regex = get_regex(lang)
280
281
  self.initial_lang = lang
282
+ self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
281
283
  if 'betterproto' not in sys.modules:
282
284
  logger.warning('betterproto not available, Google Lens will not work!')
283
285
  else:
@@ -375,6 +377,8 @@ class GoogleLens:
375
377
  for line in paragraph['lines']:
376
378
  if furigana_filter_sensitivity:
377
379
  for word in line['words']:
380
+ if not self.punctuation_regex.findall(word):
381
+ continue
378
382
  if 'geometry' not in word:
379
383
  res += word['plain_text'] + word['text_separator']
380
384
  continue
@@ -383,7 +387,7 @@ class GoogleLens:
383
387
  if word_width > furigana_filter_sensitivity and word_height > furigana_filter_sensitivity:
384
388
  res += word['plain_text'] + word['text_separator']
385
389
  else:
386
- skipped.extend([word['plain_text'] for word in line['words']])
390
+ skipped.extend(word['plain_text'])
387
391
  continue
388
392
  else:
389
393
  for word in line['words']:
@@ -935,10 +939,10 @@ class OneOCR:
935
939
  if sys.platform == 'win32':
936
940
  try:
937
941
  ocr_resp = self.model.recognize_pil(img)
938
- if os.path.exists(os.path.expanduser("~/GSM/temp")):
939
- with open(os.path.join(os.path.expanduser("~/GSM/temp"), 'oneocr_response.json'), 'w',
940
- encoding='utf-8') as f:
941
- json.dump(ocr_resp, f, indent=4, ensure_ascii=False)
942
+ # if os.path.exists(os.path.expanduser("~/GSM/temp")):
943
+ # with open(os.path.join(os.path.expanduser("~/GSM/temp"), 'oneocr_response.json'), 'w',
944
+ # encoding='utf-8') as f:
945
+ # json.dump(ocr_resp, f, indent=4, ensure_ascii=False)
942
946
  # print(json.dumps(ocr_resp))
943
947
  filtered_lines = [line for line in ocr_resp['lines'] if self.regex.search(line['text'])]
944
948
  x_coords = [line['bounding_rect'][f'x{i}'] for line in filtered_lines for i in range(1, 5)]
@@ -1402,6 +1406,11 @@ class localLLMOCR:
1402
1406
 
1403
1407
  def __init__(self, config={}, lang='ja'):
1404
1408
  self.keep_llm_hot_thread = None
1409
+ # All three config values are required: url, model, api_key
1410
+ if not config or not (config.get('url') and config.get('model') and config.get('api_key')):
1411
+ logger.warning('Local LLM OCR requires url, model, and api_key in config, Local LLM OCR will not work!')
1412
+ return
1413
+
1405
1414
  try:
1406
1415
  import openai
1407
1416
  except ImportError:
@@ -1409,16 +1418,20 @@ class localLLMOCR:
1409
1418
  return
1410
1419
  import openai, threading
1411
1420
  try:
1412
- self.api_url = config.get('api_url', 'http://localhost:1234/v1/chat/completions')
1421
+ self.api_url = config.get('url', 'http://localhost:1234/v1/chat/completions')
1413
1422
  self.model = config.get('model', 'qwen2.5-vl-3b-instruct')
1414
1423
  self.api_key = config.get('api_key', 'lm-studio')
1415
1424
  self.keep_warm = config.get('keep_warm', True)
1416
1425
  self.custom_prompt = config.get('prompt', None)
1417
1426
  self.available = True
1427
+ if any(x in self.api_url for x in ['localhost', '127.0.0.1']):
1428
+ if not self.check_connection(self.api_url):
1429
+ logger.warning('Local LLM OCR API is not reachable')
1430
+ return
1418
1431
  self.client = openai.OpenAI(
1419
- base_url=self.api_url.replace('/v1/chat/completions', '/v1'),
1420
- api_key=self.api_key
1421
- )
1432
+ base_url=self.api_url.replace('/v1/chat/completions', '/v1'),
1433
+ api_key=self.api_key
1434
+ )
1422
1435
  if self.client.models.retrieve(self.model):
1423
1436
  self.model = self.model
1424
1437
  logger.info(f'Local LLM OCR (OpenAI-compatible) ready with model {self.model}')
@@ -1427,6 +1440,25 @@ class localLLMOCR:
1427
1440
  self.keep_llm_hot_thread.start()
1428
1441
  except Exception as e:
1429
1442
  logger.warning(f'Error initializing Local LLM OCR, Local LLM OCR will not work!')
1443
+
1444
+ def check_connection(self, url, port=None):
1445
+ # simple connectivity check with mega low timeout
1446
+ import http.client
1447
+ conn = http.client.HTTPConnection(url, port or 1234, timeout=0.1)
1448
+ try:
1449
+ conn.request("GET", "/v1/models")
1450
+ response = conn.getresponse()
1451
+ if response.status == 200:
1452
+ logger.info('Local LLM OCR API is reachable')
1453
+ return True
1454
+ else:
1455
+ logger.warning('Local LLM OCR API is not reachable')
1456
+ return False
1457
+ except Exception as e:
1458
+ logger.warning(f'Error connecting to Local LLM OCR API: {e}')
1459
+ return False
1460
+ finally:
1461
+ conn.close()
1430
1462
 
1431
1463
  def keep_llm_warm(self):
1432
1464
  def ocr_blank_black_image():