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.
- GameSentenceMiner/ai/ai_prompting.py +23 -22
- GameSentenceMiner/anki.py +8 -8
- GameSentenceMiner/config_gui.py +111 -49
- GameSentenceMiner/locales/en_us.json +1 -1
- GameSentenceMiner/obs.py +42 -5
- GameSentenceMiner/ocr/gsm_ocr_config.py +8 -2
- GameSentenceMiner/ocr/owocr_helper.py +9 -1
- GameSentenceMiner/owocr/owocr/ocr.py +41 -9
- GameSentenceMiner/owocr/owocr/run.py +209 -26
- GameSentenceMiner/util/configuration.py +6 -0
- GameSentenceMiner/util/electron_config.py +2 -2
- GameSentenceMiner/util/get_overlay_coords.py +17 -3
- GameSentenceMiner/web/templates/index.html +19 -19
- GameSentenceMiner/web/texthooking_page.py +30 -0
- gamesentenceminer-2.14.6.dist-info/METADATA +169 -0
- {gamesentenceminer-2.14.4.dist-info → gamesentenceminer-2.14.6.dist-info}/RECORD +20 -20
- gamesentenceminer-2.14.4.dist-info/METADATA +0 -46
- {gamesentenceminer-2.14.4.dist-info → gamesentenceminer-2.14.6.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.14.4.dist-info → gamesentenceminer-2.14.6.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.14.4.dist-info → gamesentenceminer-2.14.6.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.14.4.dist-info → gamesentenceminer-2.14.6.dist-info}/top_level.txt +0 -0
@@ -24,13 +24,13 @@ TRANSLATION_PROMPT = f"""
|
|
24
24
|
**Professional Game Localization Task**
|
25
25
|
|
26
26
|
**Task Directive:**
|
27
|
-
Translate ONLY the
|
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
|
-
-
|
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
|
-
|
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,
|
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
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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"]))
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -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.
|
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=
|
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
|
-
|
882
|
-
HoverInfoLabelWidget(required_settings_frame, text=
|
883
|
-
tooltip=
|
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
|
-
|
1559
|
-
|
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
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
|
2136
|
-
|
2137
|
-
|
2138
|
-
|
2139
|
-
|
2140
|
-
|
2141
|
-
|
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
|
-
|
2160
|
-
|
2161
|
-
|
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(
|
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
|
-
|
252
|
-
if
|
253
|
-
|
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
|
-
|
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(
|
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
|
-
|
940
|
-
|
941
|
-
|
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('
|
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
|
-
|
1420
|
-
|
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():
|