GameSentenceMiner 2.14.3__py3-none-any.whl → 2.14.5__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 +26 -24
- 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_area_selector.py +17 -0
- GameSentenceMiner/owocr/owocr/ocr.py +42 -9
- GameSentenceMiner/owocr/owocr/run.py +209 -26
- GameSentenceMiner/util/configuration.py +6 -0
- GameSentenceMiner/util/electron_config.py +2 -2
- GameSentenceMiner/web/templates/index.html +19 -19
- GameSentenceMiner/web/texthooking_page.py +30 -0
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/METADATA +9 -4
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/RECORD +19 -19
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.14.3.dist-info → gamesentenceminer-2.14.5.dist-info}/top_level.txt +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
|
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
|
+
- 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
|
-
|
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=
|
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=
|
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)
|
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=""):
|
@@ -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]
|