GameSentenceMiner 2.5.14__py3-none-any.whl → 2.6.1__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/__init__.py +0 -0
- GameSentenceMiner/ai/gemini.py +143 -0
- GameSentenceMiner/anki.py +13 -1
- GameSentenceMiner/config_gui.py +76 -2
- GameSentenceMiner/configuration.py +14 -0
- GameSentenceMiner/ffmpeg.py +35 -15
- GameSentenceMiner/gametext.py +30 -5
- GameSentenceMiner/gsm.py +14 -10
- GameSentenceMiner/obs.py +0 -1
- GameSentenceMiner/util.py +1 -1
- GameSentenceMiner/vad/silero_trim.py +2 -2
- GameSentenceMiner/vad/vosk_helper.py +2 -2
- GameSentenceMiner/vad/whisper_helper.py +2 -2
- {gamesentenceminer-2.5.14.dist-info → gamesentenceminer-2.6.1.dist-info}/METADATA +2 -1
- gamesentenceminer-2.6.1.dist-info/RECORD +31 -0
- gamesentenceminer-2.5.14.dist-info/RECORD +0 -29
- {gamesentenceminer-2.5.14.dist-info → gamesentenceminer-2.6.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.5.14.dist-info → gamesentenceminer-2.6.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.5.14.dist-info → gamesentenceminer-2.6.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.5.14.dist-info → gamesentenceminer-2.6.1.dist-info}/top_level.txt +0 -0
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,17 @@ 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 note and 'fields' in note and 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
|
+
translation = translate_with_context(get_all_lines(), sentence_to_translate,
|
72
|
+
game_line.index, get_current_game())
|
73
|
+
logger.info(translation)
|
74
|
+
note['fields']['SentenceMeaning'] = translation
|
75
|
+
else:
|
76
|
+
logger.error("Invalid note object. Cannot update SentenceMeaning.")
|
77
|
+
|
66
78
|
if prev_screenshot_in_anki:
|
67
79
|
note['fields'][get_config().anki.previous_image_field] = prev_screenshot_html
|
68
80
|
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -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
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -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,
|
21
|
-
|
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
|
-
"-
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|
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
|
-
|
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
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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):
|
GameSentenceMiner/gametext.py
CHANGED
@@ -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(
|
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.
|
234
|
+
command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
|
230
235
|
else:
|
231
|
-
command.
|
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.
|
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.
|
3
|
+
Version: 2.6.1
|
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=6cXCPJRp_en0ZopRCaH7vXJkfdRDFftsX15yLvCvNmI,14082
|
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.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
27
|
+
gamesentenceminer-2.6.1.dist-info/METADATA,sha256=d9eL6-5Xs0DRGNJPFY3wol_7u3eInO1T5obTwdWhvxQ,5467
|
28
|
+
gamesentenceminer-2.6.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
29
|
+
gamesentenceminer-2.6.1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
30
|
+
gamesentenceminer-2.6.1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
31
|
+
gamesentenceminer-2.6.1.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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|