GameSentenceMiner 2.5.14__py3-none-any.whl → 2.6.0__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.
File without changes
@@ -0,0 +1,143 @@
1
+ import google.generativeai as genai
2
+
3
+ from GameSentenceMiner.configuration import get_config, logger
4
+
5
+ MODEL = "gemini-2.0-flash" # or "gemini-pro-vision" if you need image support
6
+
7
+ genai.configure(api_key=get_config().ai.api_key)
8
+ model = genai.GenerativeModel(MODEL)
9
+
10
+ def translate_with_context(lines, sentence, current_line_index, game_title=""):
11
+ """
12
+ Translates a line of dialogue with context from surrounding lines.
13
+
14
+ Args:
15
+ lines: A list of strings representing the dialogue lines.
16
+ sentence: Sentence to get translation for
17
+ current_line_index: The index of the line to translate.
18
+ game_title: Optional title of the game for added context.
19
+
20
+ Returns:
21
+ A string containing the translated sentence with context.
22
+ """
23
+
24
+ if not lines or current_line_index < 0 or current_line_index >= len(lines):
25
+ return "Invalid input."
26
+
27
+ context_lines = []
28
+
29
+ # Get the previous 10 lines (or fewer if at the beginning)
30
+ for i in range(max(0, current_line_index - 10), current_line_index):
31
+ context_lines.append(lines[i].text)
32
+
33
+ # Get the current line
34
+ current_line = lines[current_line_index]
35
+ context_lines.append(current_line.text)
36
+
37
+ #Get the next 10 lines (or fewer if at the end)
38
+ for i in range(current_line_index + 1, min(current_line_index + 11, len(lines))):
39
+ context_lines.append(lines[i].text)
40
+
41
+ ai_config = get_config().ai
42
+
43
+ #this is ugly, but prettier in the output... so idk
44
+ if ai_config.use_canned_translation_prompt:
45
+ prompt_to_use = \
46
+ f"""
47
+ Translate the following Japanese dialogue from the game {game_title} into natural, context-aware English. Focus on preserving the tone, intent, and emotional nuance of the original text, paying close attention to the context provided by surrounding lines. The dialogue may include slang, idioms, implied meanings, or game-specific terminology that should be adapted naturally for English-speaking players. Ensure the translation feels immersive and aligns with the game's narrative style and character voices.
48
+ Translate only the specified line below, providing a single result. Do not include additional text, explanations, or other lines unless explicitly requested. Allow expletives if more natural. Allow HTML tags for emphasis, italics, and other formatting as needed. Please also try to preserve existing HTML tags from the specified sentence if appropriate.
49
+
50
+ Line to Translate:
51
+ """
52
+ elif ai_config.use_canned_context_prompt:
53
+ prompt_to_use = \
54
+ f"""
55
+ Provide a very brief summary of the scene in English based on the provided Japanese dialogue and context. Focus on the characters' actions and the immediate situation being described.
56
+
57
+ Current Sentence:
58
+ """
59
+ else:
60
+ prompt_to_use = ai_config.custom_prompt
61
+
62
+
63
+ prompt = \
64
+ f"""
65
+ Dialogue Context:
66
+
67
+ {chr(10).join(context_lines)}
68
+
69
+ I am playing the game {game_title}. With that, and the above dialogue context in mind, answer the following prompt.
70
+
71
+ {prompt_to_use}
72
+
73
+ {sentence}
74
+ """
75
+
76
+ logger.debug(prompt)
77
+ try:
78
+ response = model.generate_content(prompt)
79
+ return response.text.strip()
80
+ except Exception as e:
81
+ return f"Translation failed: {e}"
82
+
83
+ # Example Usage: Zero Escape: 999 examples
84
+
85
+ # zero_escape_dialogue1 = [
86
+ # "扉は開いた…?",
87
+ # "まさか、こんな仕掛けが…",
88
+ # "一体、何が起こっているんだ?",
89
+ # "この数字の意味は…?",
90
+ # "落ち着いて、考えるんだ。",
91
+ # "でも、時間が…",
92
+ # "まだ、諦めるな!",
93
+ # "一体、誰が…?",
94
+ # "まさか、あの人が…?",
95
+ # "もう、ダメだ…",
96
+ # "まだ、希望はある!",
97
+ # "この部屋から、脱出するんだ!",
98
+ # "でも、どうやって…?",
99
+ # "何か、手がかりがあるはずだ!",
100
+ # "早く、見つけないと…",
101
+ # ]
102
+ #
103
+ #
104
+ # current_line_index = 3
105
+ # translation = translate_with_context(zero_escape_dialogue1, current_line_index, "Zero Escape: 999")
106
+ # print(f"Original: {zero_escape_dialogue1[current_line_index]}")
107
+ # print(f"Translation: {translation}")
108
+ #
109
+ # # Example with fewer context lines at the beginning.
110
+ # zero_escape_dialogue2 = [
111
+ # "このアミュレット…",
112
+ # "何かを感じる…",
113
+ # "この数字は…",
114
+ # "9…?",
115
+ # "まさか、これは…",
116
+ # "何かの手がかり…?",
117
+ # "急がないと…",
118
+ # "時間がない…",
119
+ # "早く、脱出を…",
120
+ # ]
121
+ #
122
+ # current_line_index = 3
123
+ # translation = translate_with_context(zero_escape_dialogue2, current_line_index, "Zero Escape: 999")
124
+ # print(f"Original: {zero_escape_dialogue2[current_line_index]}")
125
+ # print(f"Translation: {translation}")
126
+ #
127
+ # #example with fewer context lines at the end.
128
+ # zero_escape_dialogue3 = [
129
+ # "この状況、理解できない。",
130
+ # "誰かが、私たちを閉じ込めたのか?",
131
+ # "なぜ、こんなことを…?",
132
+ # "このゲームの目的は…?",
133
+ # "一体、何が真実なんだ?",
134
+ # "信じられるのは、誰…?",
135
+ # "疑心暗鬼になるな。",
136
+ # "でも、どうすれば…?",
137
+ # "とにかく、進むしかない。"
138
+ # ]
139
+ #
140
+ # current_line_index = 4
141
+ # translation = translate_with_context(zero_escape_dialogue3, current_line_index, "Zero Escape: 999")
142
+ # print(f"Original: {zero_escape_dialogue3[current_line_index]}")
143
+ # print(f"Translation: {translation}")
GameSentenceMiner/anki.py CHANGED
@@ -8,9 +8,10 @@ from datetime import datetime, timedelta
8
8
  from requests import post
9
9
 
10
10
  from GameSentenceMiner import obs, util, notification, ffmpeg
11
+ from GameSentenceMiner.ai.gemini import translate_with_context
11
12
  from GameSentenceMiner.configuration import *
12
13
  from GameSentenceMiner.configuration import get_config
13
- from GameSentenceMiner.gametext import get_text_event
14
+ from GameSentenceMiner.gametext import get_text_event, get_all_lines
14
15
  from GameSentenceMiner.model import AnkiCard
15
16
  from GameSentenceMiner.utility_gui import get_utility_window
16
17
  from GameSentenceMiner.obs import get_current_game
@@ -63,6 +64,19 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
63
64
  if not get_config().screenshot.enabled:
64
65
  logger.info("Skipping Adding Screenshot to Anki, Screenshot is disabled in settings")
65
66
 
67
+ if get_config().ai.enabled:
68
+ sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
69
+ sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
70
+ get_config().anki.sentence_field)
71
+
72
+ if note and 'fields' in note:
73
+ translation = translate_with_context(get_all_lines(), sentence_to_translate,
74
+ game_line.index, get_current_game())
75
+ logger.info(translation)
76
+ note['fields']['SentenceMeaning'] = translation
77
+ else:
78
+ logger.error("Invalid note object. Cannot update SentenceMeaning.")
79
+
66
80
  if prev_screenshot_in_anki:
67
81
  note['fields'][get_config().anki.previous_image_field] = prev_screenshot_html
68
82
 
@@ -1,5 +1,5 @@
1
1
  import tkinter as tk
2
- from tkinter import filedialog, messagebox, simpledialog
2
+ from tkinter import filedialog, messagebox, simpledialog, scrolledtext
3
3
 
4
4
  import ttkbootstrap as ttk
5
5
 
@@ -77,6 +77,7 @@ class ConfigApp:
77
77
  self.create_hotkeys_tab()
78
78
  self.create_profiles_tab()
79
79
  self.create_advanced_tab()
80
+ self.create_ai_tab()
80
81
 
81
82
  ttk.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
82
83
 
@@ -155,7 +156,8 @@ class ConfigApp:
155
156
  custom_ffmpeg_settings=self.screenshot_custom_ffmpeg_settings.get(),
156
157
  screenshot_hotkey_updates_anki=self.screenshot_hotkey_update_anki.get(),
157
158
  seconds_after_line = self.seconds_after_line.get(),
158
- use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get()
159
+ use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get(),
160
+ use_new_screenshot_logic=self.use_new_screenshot_logic.get()
159
161
  ),
160
162
  audio=Audio(
161
163
  enabled=self.audio_enabled.get(),
@@ -199,6 +201,15 @@ class ConfigApp:
199
201
  show_screenshot_buttons=self.show_screenshot_button.get(),
200
202
  multi_line_line_break=self.multi_line_line_break.get(),
201
203
  multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
204
+ ),
205
+ ai=Ai(
206
+ enabled=self.ai_enabled.get(),
207
+ # provider=self.provider.get(),
208
+ anki_field=self.ai_anki_field.get(),
209
+ api_key=self.ai_api_key.get(),
210
+ use_canned_translation_prompt=self.use_canned_translation_prompt.get(),
211
+ use_canned_context_prompt=self.use_canned_context_prompt.get(),
212
+ custom_prompt=self.custom_prompt.get("1.0", tk.END)
202
213
  )
203
214
  )
204
215
 
@@ -262,6 +273,7 @@ class ConfigApp:
262
273
  self.create_hotkeys_tab()
263
274
  self.create_profiles_tab()
264
275
  self.create_advanced_tab()
276
+ self.create_ai_tab()
265
277
 
266
278
 
267
279
  def increment_row(self):
@@ -739,6 +751,11 @@ class ConfigApp:
739
751
  ttk.Checkbutton(screenshot_frame, variable=self.use_beginning_of_line_as_screenshot).grid(row=self.current_row, column=1, sticky='W')
740
752
  self.add_label_and_increment_row(screenshot_frame, "Enable to use the beginning of the line as the screenshot point. Adjust the above setting to fine-tine timing.", row=self.current_row, column=2)
741
753
 
754
+ ttk.Label(screenshot_frame, text="Use alternative screenshot logic:").grid(row=self.current_row, column=0, sticky='W')
755
+ self.use_new_screenshot_logic = tk.BooleanVar(value=self.settings.screenshot.use_new_screenshot_logic)
756
+ ttk.Checkbutton(screenshot_frame, variable=self.use_new_screenshot_logic).grid(row=self.current_row, column=1, sticky='W')
757
+ self.add_label_and_increment_row(screenshot_frame, "Enable to use the new screenshot logic. This will try to take the screenshot in the middle of the voiceline, or middle of the line if no audio/vad.", row=self.current_row, column=2)
758
+
742
759
  @new_tab
743
760
  def create_audio_tab(self):
744
761
  audio_frame = ttk.Frame(self.notebook)
@@ -953,6 +970,63 @@ class ConfigApp:
953
970
  self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1)
954
971
  self.add_label_and_increment_row(advanced_frame, "Field in Anki for storing the multi-line sentence temporarily.", row=self.current_row, column=2)
955
972
 
973
+
974
+ @new_tab
975
+ def create_ai_tab(self):
976
+ ai_frame = ttk.Frame(self.notebook)
977
+ self.notebook.add(ai_frame, text='AI')
978
+
979
+ ttk.Label(ai_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
980
+ self.ai_enabled = tk.BooleanVar(value=self.settings.ai.enabled)
981
+ ttk.Checkbutton(ai_frame, variable=self.ai_enabled).grid(row=self.current_row, column=1, sticky='W')
982
+ self.add_label_and_increment_row(ai_frame, "Enable or disable AI integration.", row=self.current_row, column=2)
983
+
984
+ ttk.Label(ai_frame, text="Anki Field:").grid(row=self.current_row, column=0, sticky='W')
985
+ self.ai_anki_field = ttk.Entry(ai_frame)
986
+ self.ai_anki_field.insert(0, self.settings.ai.anki_field)
987
+ self.ai_anki_field.grid(row=self.current_row, column=1)
988
+ self.add_label_and_increment_row(ai_frame, "Field in Anki for AI-generated content.", row=self.current_row,
989
+ column=2)
990
+
991
+ # ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
992
+ # self.provider = ttk.Combobox(ai_frame,
993
+ # values=[AI_GEMINI])
994
+ # self.provider.set(self.settings.ai.provider)
995
+ # self.provider.grid(row=self.current_row, column=1)
996
+ # self.add_label_and_increment_row(ai_frame, "Select the AI provider. Currently only Gemini is supported.", row=self.current_row, column=2)
997
+
998
+ ttk.Label(ai_frame, text="API Key:").grid(row=self.current_row, column=0, sticky='W')
999
+ self.ai_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1000
+ self.ai_api_key.insert(0, self.settings.ai.api_key)
1001
+ self.ai_api_key.grid(row=self.current_row, column=1)
1002
+ self.add_label_and_increment_row(ai_frame, "API key for the selected AI provider (Gemini only currently).", row=self.current_row,
1003
+ column=2)
1004
+
1005
+ ttk.Label(ai_frame, text="Use Canned Translation Prompt:").grid(row=self.current_row, column=0, sticky='W')
1006
+ self.use_canned_translation_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_translation_prompt)
1007
+ ttk.Checkbutton(ai_frame, variable=self.use_canned_translation_prompt).grid(row=self.current_row, column=1,
1008
+ sticky='W')
1009
+ self.add_label_and_increment_row(ai_frame, "Use a pre-defined translation prompt for AI.", row=self.current_row,
1010
+ column=2)
1011
+
1012
+ ttk.Label(ai_frame, text="Use Canned Context Prompt:").grid(row=self.current_row, column=0, sticky='W')
1013
+ self.use_canned_context_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_context_prompt)
1014
+ ttk.Checkbutton(ai_frame, variable=self.use_canned_context_prompt).grid(row=self.current_row, column=1,
1015
+ sticky='W')
1016
+ self.add_label_and_increment_row(ai_frame, "Use a pre-defined context prompt for AI.", row=self.current_row,
1017
+ column=2)
1018
+
1019
+ ttk.Label(ai_frame, text="Custom Prompt:").grid(row=self.current_row, column=0, sticky='W')
1020
+
1021
+ self.custom_prompt = scrolledtext.ScrolledText(ai_frame, width=50, height=5) # Adjust height as needed
1022
+ self.custom_prompt.insert(tk.END, self.settings.ai.custom_prompt)
1023
+ self.custom_prompt.grid(row=self.current_row, column=1)
1024
+
1025
+ self.add_label_and_increment_row(ai_frame, "Custom prompt for AI processing.", row=self.current_row, column=2)
1026
+
1027
+ return ai_frame
1028
+
1029
+
956
1030
  def on_profile_change(self, event):
957
1031
  print("profile Changed!")
958
1032
  self.save_settings(profile_change=True)
@@ -27,6 +27,8 @@ WHISPER_SMALL = 'small'
27
27
  WHISPER_MEDIUM = 'medium'
28
28
  WHSIPER_LARGE = 'large'
29
29
 
30
+ AI_GEMINI = 'gemini'
31
+
30
32
  INFO = 'INFO'
31
33
  DEBUG = 'DEBUG'
32
34
 
@@ -107,6 +109,7 @@ class Screenshot:
107
109
  screenshot_hotkey_updates_anki: bool = False
108
110
  seconds_after_line: float = 1.0
109
111
  use_beginning_of_line_as_screenshot: bool = True
112
+ use_new_screenshot_logic: bool = False
110
113
 
111
114
 
112
115
  @dataclass_json
@@ -175,6 +178,16 @@ class Advanced:
175
178
  multi_line_line_break: str = '<br>'
176
179
  multi_line_sentence_storage_field: str = ''
177
180
 
181
+ @dataclass_json
182
+ @dataclass
183
+ class Ai:
184
+ enabled: bool = False
185
+ anki_field: str = ''
186
+ provider: str = AI_GEMINI
187
+ api_key: str = ''
188
+ use_canned_translation_prompt: bool = True
189
+ use_canned_context_prompt: bool = False
190
+ custom_prompt: str = ''
178
191
 
179
192
 
180
193
  @dataclass_json
@@ -191,6 +204,7 @@ class ProfileConfig:
191
204
  hotkeys: Hotkeys = field(default_factory=Hotkeys)
192
205
  vad: VAD = field(default_factory=VAD)
193
206
  advanced: Advanced = field(default_factory=Advanced)
207
+ ai: Ai = field(default_factory=Ai)
194
208
 
195
209
 
196
210
  # This is just for legacy support
@@ -17,13 +17,13 @@ def get_ffprobe_path():
17
17
  ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
18
18
 
19
19
 
20
- def get_screenshot(video_file, time_from_end):
21
- time_from_end_to_capture = -time_from_end if time_from_end else -1
20
+ def get_screenshot(video_file, screenshot_timing):
21
+ screenshot_timing = screenshot_timing if screenshot_timing else 1
22
22
  output_image = make_unique_file_name(os.path.join(
23
23
  get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
24
24
  # FFmpeg command to extract the last frame of the video
25
25
  ffmpeg_command = ffmpeg_base_command_list + [
26
- "-sseof", f"{time_from_end_to_capture}", # Seek to 1 second before the end of the video
26
+ "-ss", f"{screenshot_timing}", # Seek to 1 second after the beginning
27
27
  "-i", f"{video_file}",
28
28
  "-vframes", "1" # Extract only one frame
29
29
  ]
@@ -52,31 +52,51 @@ def get_screenshot_for_line(video_file, game_line):
52
52
  return get_screenshot(video_file, get_screenshot_time(video_file, game_line))
53
53
 
54
54
 
55
-
56
- def get_screenshot_time(video_path, game_line, default_beginning=False):
55
+ def get_screenshot_time(video_path, game_line, default_beginning=False, vad_beginning=None, vad_end=None):
57
56
  if game_line:
58
57
  line_time = game_line.time
59
58
  else:
59
+ # Assuming initial_time is defined elsewhere if game_line is None
60
60
  line_time = initial_time
61
61
 
62
62
  file_length = get_video_duration(video_path)
63
63
  file_mod_time = get_file_modification_time(video_path)
64
64
 
65
+ # Calculate when the line occurred within the video file (seconds from start)
65
66
  time_delta = file_mod_time - line_time
66
- total_seconds = file_length - time_delta.total_seconds()
67
-
68
- time_from_end = file_length - total_seconds - get_config().screenshot.seconds_after_line
67
+ line_timestamp_in_video = file_length - time_delta.total_seconds()
68
+ screenshot_offset = get_config().screenshot.seconds_after_line
69
+
70
+ # Calculate screenshot time from the beginning by adding the offset
71
+ if vad_beginning and vad_end:
72
+ logger.debug("Using VAD to determine screenshot time")
73
+ screenshot_time_from_beginning = line_timestamp_in_video + vad_end - screenshot_offset
74
+ elif get_config().screenshot.use_new_screenshot_logic:
75
+ if game_line.next:
76
+ logger.debug("Finding time between lines for screenshot")
77
+ screenshot_time_from_beginning = line_timestamp_in_video + ((game_line.next.time - game_line.time).total_seconds() / 2)
78
+ else:
79
+ logger.debug("Using end of line for screenshot")
80
+ screenshot_time_from_beginning = file_length - screenshot_offset
81
+ else:
82
+ screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
69
83
 
70
- if time_from_end < 0 or time_from_end > file_length:
84
+ # Check if the calculated time is out of bounds
85
+ if screenshot_time_from_beginning < 0 or screenshot_time_from_beginning > file_length:
71
86
  logger.error(
72
- "Calculated screenshot time is out of bounds for trimmed video")
87
+ f"Calculated screenshot time ({screenshot_time_from_beginning:.2f}s) is out of bounds for video (length {file_length:.2f}s)."
88
+ )
73
89
  if default_beginning:
74
- logger.info("Defaulting to using the beginning of the Replay Buffer")
75
- return file_length - 1.0
76
- logger.info("Defaulting to using the end of the Replay Buffer")
77
- return 0
90
+ logger.info("Defaulting to using the beginning of the video (1.0s)")
91
+ # Return time for the start of the video
92
+ return 1.0
93
+ logger.info(f"Defaulting to using the end of the video ({file_length:.2f}s)")
94
+ return file_length - screenshot_offset
95
+
96
+ logger.info("Screenshot time from beginning: " + str(screenshot_time_from_beginning))
78
97
 
79
- return time_from_end
98
+ # Return the calculated time from the beginning
99
+ return screenshot_time_from_beginning
80
100
 
81
101
 
82
102
  def process_image(image_file):
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
 
7
7
  import pyperclip
8
8
  import websockets
9
+ from websockets import InvalidStatusCode
9
10
 
10
11
  from GameSentenceMiner import util
11
12
  from GameSentenceMiner.model import AnkiCard
@@ -30,6 +31,7 @@ class GameLine:
30
31
  time: datetime
31
32
  prev: 'GameLine'
32
33
  next: 'GameLine'
34
+ index: int = 0
33
35
 
34
36
  def get_previous_time(self):
35
37
  if self.prev:
@@ -44,6 +46,7 @@ class GameLine:
44
46
  @dataclass
45
47
  class GameText:
46
48
  values: list[GameLine]
49
+ game_line_index = 0
47
50
 
48
51
  def __init__(self):
49
52
  self.values = []
@@ -64,7 +67,8 @@ class GameText:
64
67
  return None
65
68
 
66
69
  def add_line(self, line_text):
67
- new_line = GameLine(line_text, datetime.now(), self.values[-1] if self.values else None, None)
70
+ new_line = GameLine(line_text, datetime.now(), self.values[-1] if self.values else None, None, self.game_line_index)
71
+ self.game_line_index += 1
68
72
  if self.values:
69
73
  self.values[-1].next = new_line
70
74
  self.values.append(new_line)
@@ -106,25 +110,38 @@ class ClipboardMonitor(threading.Thread):
106
110
 
107
111
  async def listen_websocket():
108
112
  global current_line, current_line_time, line_history, reconnecting, websocket_connected
113
+ try_other = False
114
+ websocket_url = f'ws://{get_config().general.websocket_uri}'
109
115
  while True:
116
+ if try_other:
117
+ websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
110
118
  try:
111
- async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
119
+ async with websockets.connect(websocket_url, ping_interval=None) as websocket:
120
+ logger.info("TextHooker Websocket Connected!")
112
121
  if reconnecting:
113
122
  logger.info(f"Texthooker WebSocket connected Successfully!" + " Disabling Clipboard Monitor." if get_config().general.use_clipboard else "")
114
123
  reconnecting = False
115
124
  websocket_connected = True
125
+ try_other = True
116
126
  while True:
117
127
  message = await websocket.recv()
118
-
128
+ logger.debug(message)
119
129
  try:
120
130
  data = json.loads(message)
121
131
  if "sentence" in data:
122
132
  current_clipboard = data["sentence"]
123
- except json.JSONDecodeError:
133
+ except json.JSONDecodeError or TypeError:
124
134
  current_clipboard = message
125
135
  if current_clipboard != current_line:
126
136
  handle_new_text_event(current_clipboard)
127
- except (websockets.ConnectionClosed, ConnectionError) as e:
137
+ except (websockets.ConnectionClosed, ConnectionError, InvalidStatusCode) as e:
138
+ if isinstance(e, InvalidStatusCode):
139
+ e: InvalidStatusCode
140
+ if e.status_code == 404:
141
+ logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
142
+ try_other = True
143
+
144
+ logger.error(f"Texthooker WebSocket connection failed. Please check if the Texthooker is running and the WebSocket URI is correct.")
128
145
  websocket_connected = False
129
146
  if not reconnecting:
130
147
  logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
@@ -225,3 +242,11 @@ def get_mined_line(last_note: AnkiCard, lines):
225
242
 
226
243
  def get_time_of_line(line):
227
244
  return line_history.get_time(line)
245
+
246
+
247
+ def get_all_lines():
248
+ return line_history.values
249
+
250
+
251
+ def get_line_history():
252
+ return line_history
GameSentenceMiner/gsm.py CHANGED
@@ -116,9 +116,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
116
116
  mined_line = get_mined_line(last_note, lines)
117
117
  line_cutoff = get_utility_window().get_next_line_timing()
118
118
 
119
- ss_timing = 0
120
- if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
121
- ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line)
122
119
  if last_note:
123
120
  logger.debug(last_note.to_json())
124
121
  selected_lines = get_utility_window().get_selected_lines()
@@ -128,7 +125,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
128
125
 
129
126
  if get_config().anki.sentence_audio_field and get_config().audio.enabled:
130
127
  logger.debug("Attempting to get audio from video")
131
- final_audio_output, should_update_audio, vad_trimmed_audio = VideoToAudioHandler.get_audio(
128
+ final_audio_output, should_update_audio, vad_trimmed_audio, vad_beginning, vad_end = VideoToAudioHandler.get_audio(
132
129
  start_line,
133
130
  line_cutoff,
134
131
  video_path)
@@ -136,10 +133,17 @@ class VideoToAudioHandler(FileSystemEventHandler):
136
133
  final_audio_output = ""
137
134
  should_update_audio = False
138
135
  vad_trimmed_audio = ""
136
+ vad_beginning = 0
137
+ vad_end = 0
139
138
  if not get_config().audio.enabled:
140
139
  logger.info("Audio is disabled in config, skipping audio processing!")
141
140
  elif not get_config().anki.sentence_audio_field:
142
141
  logger.info("No SentenceAudio Field in config, skipping audio processing!")
142
+
143
+ ss_timing = 1
144
+ if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
145
+ ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_beginning, vad_end)
146
+
143
147
  if get_config().anki.update_anki and last_note:
144
148
  anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
145
149
  tango=tango,
@@ -170,10 +174,11 @@ class VideoToAudioHandler(FileSystemEventHandler):
170
174
  final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
171
175
  f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
172
176
  should_update_audio = True
177
+ vad_beginning, vad_end = 0, 0
173
178
  if get_config().vad.do_vad_postprocessing:
174
- should_update_audio = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio, vad_trimmed_audio)
179
+ should_update_audio, vad_beginning, vad_end = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio, vad_trimmed_audio)
175
180
  if not should_update_audio:
176
- should_update_audio = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio,
181
+ should_update_audio, vad_beginning, vad_end = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio,
177
182
  vad_trimmed_audio)
178
183
  if not should_update_audio and get_config().vad.add_audio_on_no_results:
179
184
  logger.info("No voice activity detected, using full audio.")
@@ -184,7 +189,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
184
189
  get_config().audio.ffmpeg_reencode_options)
185
190
  elif os.path.exists(vad_trimmed_audio):
186
191
  shutil.move(vad_trimmed_audio, final_audio_output)
187
- return final_audio_output, should_update_audio, vad_trimmed_audio
192
+ return final_audio_output, should_update_audio, vad_trimmed_audio, vad_beginning, vad_end
188
193
 
189
194
 
190
195
  def do_vad_processing(model, trimmed_audio, vad_trimmed_audio, second_pass=False):
@@ -226,10 +231,9 @@ def play_video_in_external(line, filepath):
226
231
 
227
232
  if start:
228
233
  if "vlc" in get_config().advanced.video_player_path:
229
- command.append("--start-time")
234
+ command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
230
235
  else:
231
- command.append("--start")
232
- command.append(convert_to_vlc_seconds(start))
236
+ command.extend(["--start", convert_to_vlc_seconds(start)])
233
237
  command.append(os.path.normpath(filepath))
234
238
 
235
239
  logger.info(" ".join(command))
GameSentenceMiner/obs.py CHANGED
@@ -118,7 +118,6 @@ def do_obs_call(request, from_dict = None, retry=10):
118
118
  return from_dict(response.datain)
119
119
  return None
120
120
  except Exception as e:
121
- logger.error(e)
122
121
  if "socket is already closed" in str(e) or "object has no attribute" in str(e):
123
122
  if retry > 0:
124
123
  time.sleep(1)
GameSentenceMiner/util.py CHANGED
@@ -59,7 +59,7 @@ def timedelta_to_ffmpeg_friendly_format(td_obj):
59
59
 
60
60
 
61
61
  def get_file_modification_time(file_path):
62
- mod_time_epoch = os.path.getmtime(file_path)
62
+ mod_time_epoch = os.path.getctime(file_path)
63
63
  mod_time = datetime.fromtimestamp(mod_time_epoch)
64
64
  return mod_time
65
65
 
@@ -31,7 +31,7 @@ def process_audio_with_silero(input_audio, output_audio):
31
31
 
32
32
  if not voice_activity:
33
33
  logger.info("No voice activity detected in the audio.")
34
- return False
34
+ return False, 0, 0
35
35
 
36
36
  # Trim based on the first and last speech detected
37
37
  start_time = voice_activity[0]['start'] if voice_activity else 0
@@ -40,4 +40,4 @@ def process_audio_with_silero(input_audio, output_audio):
40
40
  # Trim the audio using FFmpeg
41
41
  ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
42
42
  logger.info(f"Trimmed audio saved to: {output_audio}")
43
- return True
43
+ return True, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset
@@ -127,7 +127,7 @@ def process_audio_with_vosk(input_audio, output_audio):
127
127
 
128
128
  if not voice_activity:
129
129
  logger.info("No voice activity detected in the audio.")
130
- return False
130
+ return False, 0, 0
131
131
 
132
132
  # Trim based on the first and last speech detected
133
133
  start_time = voice_activity[0]['start'] if voice_activity else 0
@@ -142,7 +142,7 @@ def process_audio_with_vosk(input_audio, output_audio):
142
142
  # Trim the audio using FFmpeg
143
143
  ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
144
144
  logger.info(f"Trimmed audio saved to: {output_audio}")
145
- return True
145
+ return True, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset
146
146
 
147
147
 
148
148
  def get_vosk_model():
@@ -74,7 +74,7 @@ def process_audio_with_whisper(input_audio, output_audio):
74
74
 
75
75
  if not voice_activity:
76
76
  logger.info("No voice activity detected in the audio.")
77
- return False
77
+ return False, 0, 0
78
78
 
79
79
  # Trim based on the first and last speech detected
80
80
  start_time = voice_activity[0]['start']
@@ -89,7 +89,7 @@ def process_audio_with_whisper(input_audio, output_audio):
89
89
  # Trim the audio using FFmpeg
90
90
  ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
91
91
  logger.info(f"Trimmed audio saved to: {output_audio}")
92
- return True
92
+ return True, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset
93
93
 
94
94
 
95
95
  # Load Whisper model initially
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.5.14
3
+ Version: 2.6.0
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -33,6 +33,7 @@ Requires-Dist: win10toast
33
33
  Requires-Dist: numpy
34
34
  Requires-Dist: pystray
35
35
  Requires-Dist: pywin32; sys_platform == "win32"
36
+ Requires-Dist: google-generativeai
36
37
  Dynamic: license-file
37
38
 
38
39
  # Game Sentence Miner
@@ -0,0 +1,31 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=zcusTxPaSvdmE6sO1az4xTEu9qfU3dRCLjZPa23QiyQ,14087
3
+ GameSentenceMiner/config_gui.py,sha256=AifBgNaeWcBhuFoQz8QJ4srM1Ck8KjHmnOmI-aRcFHg,66965
4
+ GameSentenceMiner/configuration.py,sha256=F-WOOjofV-GrBX2SCpArw5LMqpMGf_KCOzjvQeZZlhM,20287
5
+ GameSentenceMiner/ffmpeg.py,sha256=iUj-1uLLJla6jjGDKuc1FbcXjMogYpoDhrQqCbQFcWA,13359
6
+ GameSentenceMiner/gametext.py,sha256=C8cOMKgKF3AdcaNOKSEYkdg-DAlZ8yXzhiqXJKXIgeA,8365
7
+ GameSentenceMiner/gsm.py,sha256=l4g2qmW-3Wgzd-TJ6YS8qZsN0dIiky20vNaAgnoJJnc,24716
8
+ GameSentenceMiner/model.py,sha256=JdnkT4VoPOXmOpRgFdvERZ09c9wLN6tUJxdrKlGZcqo,5305
9
+ GameSentenceMiner/notification.py,sha256=FY39ChSRK0Y8TQ6lBGsLnpZUFPtFpSy2tweeXVoV7kc,2809
10
+ GameSentenceMiner/obs.py,sha256=xaySBuh-xnShqCPB3T8Lxr_PErNOGRMM4ISolNqmXDI,7618
11
+ GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
12
+ GameSentenceMiner/util.py,sha256=W49gqYlhbzZe-17zHMFcAjNeeteXrv_USHT7aBDKSqM,6825
13
+ GameSentenceMiner/utility_gui.py,sha256=H4aOddlsrVR768RwbMzYScCziuOz1JeySUigNrPlaac,7692
14
+ GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ GameSentenceMiner/ai/gemini.py,sha256=6kTQPuRH16D-1srhrWa5uPGIy-jBqNm9eRdKBSbpiXA,5423
16
+ GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
17
+ GameSentenceMiner/communication/send.py,sha256=oOJdCS6-LNX90amkRn5FL2xqx6THGm56zHR2ntVIFTE,229
18
+ GameSentenceMiner/communication/websocket.py,sha256=pTcUe_ZZRp9REdSU4qalhPmbT_1DKa7w18j6RfFLELA,3074
19
+ GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
20
+ GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
22
+ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ GameSentenceMiner/vad/silero_trim.py,sha256=ULf3zwS-JMsY82cKF7gZxREHw8L6lgpWF2U1YqgE9Oc,1681
24
+ GameSentenceMiner/vad/vosk_helper.py,sha256=125X8C9NxFPlWWpoNsbOnEqKx8RCjXN109zNx_QXhyg,6070
25
+ GameSentenceMiner/vad/whisper_helper.py,sha256=JJ-iltCh813XdjyEw0Wn5DaErf6PDqfH0Efu1Md8cIY,3543
26
+ gamesentenceminer-2.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
+ gamesentenceminer-2.6.0.dist-info/METADATA,sha256=gAar-A4wWYPQDZXuRSeY_lDnpxInzifk8vNuxLPcE6I,5467
28
+ gamesentenceminer-2.6.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
29
+ gamesentenceminer-2.6.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
30
+ gamesentenceminer-2.6.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
31
+ gamesentenceminer-2.6.0.dist-info/RECORD,,
@@ -1,29 +0,0 @@
1
- GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=FHm76i7WYEs--szhzZjz0B6DW4JuDQopmjR3RjYBogs,13358
3
- GameSentenceMiner/config_gui.py,sha256=WVSN239lyVm5GwKzE1HXiCSCu2CNpkcDm8TGKlTJpMc,62093
4
- GameSentenceMiner/configuration.py,sha256=obBhz7aAFkhQAhIqrocv2jRTOqIJ0tFEI_saUCyMAMc,19924
5
- GameSentenceMiner/ffmpeg.py,sha256=fzOxrn-a4KqFdUY2oove164CTDOSsdPQtzqRW5f1P7c,12002
6
- GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
7
- GameSentenceMiner/gsm.py,sha256=ZdYVJKilDFEGdC3g5V5pjSr4G2JkNNDIt7BQX6yc28A,24456
8
- GameSentenceMiner/model.py,sha256=JdnkT4VoPOXmOpRgFdvERZ09c9wLN6tUJxdrKlGZcqo,5305
9
- GameSentenceMiner/notification.py,sha256=FY39ChSRK0Y8TQ6lBGsLnpZUFPtFpSy2tweeXVoV7kc,2809
10
- GameSentenceMiner/obs.py,sha256=MlxRToq5wALPI1XrD8rxEU-N8mWII91cNJWY7rUa5uI,7642
11
- GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
12
- GameSentenceMiner/util.py,sha256=Awhy57vX4NgQzygqKaGQn2EJ75T0uiXlhmINFOWlQkU,6825
13
- GameSentenceMiner/utility_gui.py,sha256=H4aOddlsrVR768RwbMzYScCziuOz1JeySUigNrPlaac,7692
14
- GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
15
- GameSentenceMiner/communication/send.py,sha256=oOJdCS6-LNX90amkRn5FL2xqx6THGm56zHR2ntVIFTE,229
16
- GameSentenceMiner/communication/websocket.py,sha256=pTcUe_ZZRp9REdSU4qalhPmbT_1DKa7w18j6RfFLELA,3074
17
- GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
18
- GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
20
- GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
22
- GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
23
- GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
24
- gamesentenceminer-2.5.14.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- gamesentenceminer-2.5.14.dist-info/METADATA,sha256=sm1RIt4O0bGoTOY3gMeVxcj7qbm_whFLk_6Jgf_blVk,5433
26
- gamesentenceminer-2.5.14.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
27
- gamesentenceminer-2.5.14.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
28
- gamesentenceminer-2.5.14.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
29
- gamesentenceminer-2.5.14.dist-info/RECORD,,