GameSentenceMiner 2.0.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.
- GameSentenceMiner/__init__.py +0 -0
- GameSentenceMiner/anki.py +265 -0
- GameSentenceMiner/config_gui.py +803 -0
- GameSentenceMiner/configuration.py +359 -0
- GameSentenceMiner/ffmpeg.py +297 -0
- GameSentenceMiner/gametext.py +128 -0
- GameSentenceMiner/gsm.py +385 -0
- GameSentenceMiner/model.py +84 -0
- GameSentenceMiner/notification.py +69 -0
- GameSentenceMiner/obs.py +128 -0
- GameSentenceMiner/util.py +136 -0
- GameSentenceMiner/vad/__init__.py +0 -0
- GameSentenceMiner/vad/silero_trim.py +43 -0
- GameSentenceMiner/vad/vosk_helper.py +152 -0
- GameSentenceMiner/vad/whisper_helper.py +98 -0
- GameSentenceMiner-2.0.0.dist-info/METADATA +346 -0
- GameSentenceMiner-2.0.0.dist-info/RECORD +20 -0
- GameSentenceMiner-2.0.0.dist-info/WHEEL +5 -0
- GameSentenceMiner-2.0.0.dist-info/entry_points.txt +2 -0
- GameSentenceMiner-2.0.0.dist-info/top_level.txt +1 -0
File without changes
|
@@ -0,0 +1,265 @@
|
|
1
|
+
import base64
|
2
|
+
import subprocess
|
3
|
+
import threading
|
4
|
+
import time
|
5
|
+
import urllib.request
|
6
|
+
|
7
|
+
import requests as req
|
8
|
+
|
9
|
+
from . import util
|
10
|
+
from . import ffmpeg
|
11
|
+
from . import notification
|
12
|
+
from . import obs
|
13
|
+
|
14
|
+
from .configuration import *
|
15
|
+
from .configuration import get_config
|
16
|
+
from .gametext import get_last_two_sentences
|
17
|
+
from .obs import get_current_game
|
18
|
+
|
19
|
+
audio_in_anki = None
|
20
|
+
screenshot_in_anki = None
|
21
|
+
|
22
|
+
# Global variables to track state
|
23
|
+
previous_note_ids = set()
|
24
|
+
first_run = True
|
25
|
+
|
26
|
+
|
27
|
+
def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
|
28
|
+
should_update_audio=True, ss_time=0):
|
29
|
+
global audio_in_anki, screenshot_in_anki
|
30
|
+
update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
|
31
|
+
last_note['fields'][get_config().anki.sentence_audio_field][
|
32
|
+
'value'] or get_config().anki.overwrite_audio)
|
33
|
+
update_picture = (get_config().anki.picture_field and get_config().anki.overwrite_picture) or not \
|
34
|
+
last_note['fields'][get_config().anki.picture_field][
|
35
|
+
'value']
|
36
|
+
|
37
|
+
if not reuse_audio:
|
38
|
+
if update_audio:
|
39
|
+
audio_in_anki = store_media_file(audio_path)
|
40
|
+
if update_picture:
|
41
|
+
screenshot = ffmpeg.get_screenshot(video_path, ss_time)
|
42
|
+
screenshot_in_anki = store_media_file(screenshot)
|
43
|
+
if get_config().paths.remove_screenshot:
|
44
|
+
os.remove(screenshot)
|
45
|
+
audio_html = f"[sound:{audio_in_anki}]"
|
46
|
+
image_html = f"<img src=\"{screenshot_in_anki}\">"
|
47
|
+
|
48
|
+
# note = {'id': last_note['noteId'], 'fields': {}}
|
49
|
+
|
50
|
+
if update_audio:
|
51
|
+
note['fields'][get_config().anki.sentence_audio_field] = audio_html
|
52
|
+
|
53
|
+
if update_picture:
|
54
|
+
note['fields'][get_config().anki.picture_field] = image_html
|
55
|
+
|
56
|
+
if get_config().anki.anki_custom_fields:
|
57
|
+
for key, value in get_config().anki.anki_custom_fields.items():
|
58
|
+
note['fields'][key] = str(value)
|
59
|
+
|
60
|
+
invoke("updateNoteFields", note=note)
|
61
|
+
tags = []
|
62
|
+
if get_config().anki.custom_tags:
|
63
|
+
tags.extend(get_config().anki.custom_tags)
|
64
|
+
if get_config().anki.add_game_tag:
|
65
|
+
tags.append(get_current_game().replace(" ", ""))
|
66
|
+
if tags:
|
67
|
+
tag_string = " ".join(tags)
|
68
|
+
invoke("addTags", tags=tag_string, notes=[last_note['noteId']])
|
69
|
+
logger.info(f"UPDATED ANKI CARD FOR {last_note['noteId']}")
|
70
|
+
if get_config().features.notify_on_update:
|
71
|
+
notification.send_notification(tango)
|
72
|
+
if get_config().features.open_anki_edit:
|
73
|
+
notification.open_anki_card(last_note['noteId'])
|
74
|
+
|
75
|
+
if get_config().audio.external_tool:
|
76
|
+
open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
|
77
|
+
|
78
|
+
|
79
|
+
def open_audio_in_external(fileabspath, shell=False):
|
80
|
+
logger.info(f"Opening audio: {fileabspath} in external Program: {get_config().audio.external_tool}")
|
81
|
+
if shell:
|
82
|
+
subprocess.Popen(f' "{get_config().audio.external_tool}" "{fileabspath}" ', shell=True)
|
83
|
+
else:
|
84
|
+
subprocess.Popen([get_config().audio.external_tool, fileabspath])
|
85
|
+
|
86
|
+
|
87
|
+
def add_image_to_card(last_note, image_path):
|
88
|
+
global screenshot_in_anki
|
89
|
+
update_picture = get_config().anki.overwrite_picture or not last_note['fields'][get_config().anki.picture_field][
|
90
|
+
'value']
|
91
|
+
|
92
|
+
if update_picture:
|
93
|
+
screenshot_in_anki = store_media_file(image_path)
|
94
|
+
if get_config().paths.remove_screenshot:
|
95
|
+
os.remove(image_path)
|
96
|
+
|
97
|
+
image_html = f"<img src=\"{screenshot_in_anki}\">"
|
98
|
+
|
99
|
+
note = {'id': last_note['noteId'], 'fields': {}}
|
100
|
+
|
101
|
+
if update_picture:
|
102
|
+
note['fields'][get_config().anki.picture_field] = image_html
|
103
|
+
|
104
|
+
invoke("updateNoteFields", note=note)
|
105
|
+
|
106
|
+
logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note['noteId']}")
|
107
|
+
|
108
|
+
|
109
|
+
def get_initial_card_info(last_note):
|
110
|
+
note = {'id': last_note['noteId'], 'fields': {}}
|
111
|
+
if not last_note:
|
112
|
+
return note
|
113
|
+
current_line, previous_line = get_last_two_sentences()
|
114
|
+
logger.debug(f"Previous Sentence {previous_line}")
|
115
|
+
logger.debug(f"Current Sentence {current_line}")
|
116
|
+
util.use_previous_audio = True
|
117
|
+
|
118
|
+
logger.debug(
|
119
|
+
f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and previous_line and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
|
120
|
+
if get_config().anki.previous_sentence_field and previous_line and not \
|
121
|
+
last_note['fields'][get_config().anki.previous_sentence_field]['value']:
|
122
|
+
note['fields'][get_config().anki.previous_sentence_field] = previous_line
|
123
|
+
return note
|
124
|
+
|
125
|
+
|
126
|
+
def store_media_file(path):
|
127
|
+
return invoke('storeMediaFile', filename=path, data=convert_to_base64(path))
|
128
|
+
|
129
|
+
|
130
|
+
def convert_to_base64(file_path):
|
131
|
+
with open(file_path, "rb") as file:
|
132
|
+
file_base64 = base64.b64encode(file.read()).decode('utf-8')
|
133
|
+
return file_base64
|
134
|
+
|
135
|
+
|
136
|
+
def request(action, **params):
|
137
|
+
return {'action': action, 'params': params, 'version': 6}
|
138
|
+
|
139
|
+
|
140
|
+
def invoke(action, **params):
|
141
|
+
request_json = json.dumps(request(action, **params)).encode('utf-8')
|
142
|
+
# if action != "storeMediaFile":
|
143
|
+
# logger.debug(f"Hitting Anki. Action: {action}. Data: {request_json}")
|
144
|
+
response = json.load(urllib.request.urlopen(urllib.request.Request(get_config().anki.url, request_json)))
|
145
|
+
if len(response) != 2:
|
146
|
+
raise Exception('response has an unexpected number of fields')
|
147
|
+
if 'error' not in response:
|
148
|
+
raise Exception('response is missing required error field')
|
149
|
+
if 'result' not in response:
|
150
|
+
raise Exception('response is missing required result field')
|
151
|
+
if response['error'] is not None:
|
152
|
+
raise Exception(response['error'])
|
153
|
+
return response['result']
|
154
|
+
|
155
|
+
|
156
|
+
def get_last_anki_card():
|
157
|
+
added_ids = invoke('findNotes', query='added:1')
|
158
|
+
if not added_ids:
|
159
|
+
return {}
|
160
|
+
last_note = invoke('notesInfo', notes=[added_ids[-1]])[0]
|
161
|
+
return last_note
|
162
|
+
|
163
|
+
|
164
|
+
def add_wildcards(expression):
|
165
|
+
return '*' + '*'.join(expression) + '*'
|
166
|
+
|
167
|
+
|
168
|
+
def get_cards_by_sentence(sentence):
|
169
|
+
sentence = sentence.replace(" ", "")
|
170
|
+
query = f'{get_config().anki.sentence_audio_field}: {get_config().anki.sentence_field}:{add_wildcards(sentence)}'
|
171
|
+
card_ids = invoke("findCards", query=query)
|
172
|
+
|
173
|
+
if not card_ids:
|
174
|
+
print(f"Didn't find any cards matching query:\n{query}")
|
175
|
+
return {}
|
176
|
+
if len(card_ids) > 1:
|
177
|
+
print(f'Found more than 1, and not updating cards for query: \n{query}')
|
178
|
+
return {}
|
179
|
+
|
180
|
+
last_notes = invoke('notesInfo', notes=[card_ids[0]])[0]
|
181
|
+
|
182
|
+
print(f"Found Card to backfill!: {card_ids[0]}")
|
183
|
+
|
184
|
+
return last_notes
|
185
|
+
|
186
|
+
|
187
|
+
# Check for new Anki cards and save replay buffer if detected
|
188
|
+
def check_for_new_cards():
|
189
|
+
global previous_note_ids, first_run
|
190
|
+
current_note_ids = set()
|
191
|
+
try:
|
192
|
+
current_note_ids = get_note_ids()
|
193
|
+
except Exception as e:
|
194
|
+
print(f"Error fetching Anki notes: {e}")
|
195
|
+
return
|
196
|
+
new_card_ids = current_note_ids - previous_note_ids
|
197
|
+
if new_card_ids and not first_run:
|
198
|
+
update_new_card()
|
199
|
+
first_run = False
|
200
|
+
previous_note_ids = current_note_ids # Update the list of known notes
|
201
|
+
|
202
|
+
|
203
|
+
def update_new_card():
|
204
|
+
last_card = get_last_anki_card()
|
205
|
+
if not check_tags_for_should_update(last_card):
|
206
|
+
logger.info("Card not tagged properly! Not updating!")
|
207
|
+
return
|
208
|
+
|
209
|
+
use_prev_audio = util.use_previous_audio
|
210
|
+
if util.lock.locked():
|
211
|
+
print("Audio still being Trimmed, Card Queued!")
|
212
|
+
use_prev_audio = True
|
213
|
+
with util.lock:
|
214
|
+
print(f"use previous audio: {use_prev_audio}")
|
215
|
+
if get_config().obs.get_game_from_scene:
|
216
|
+
obs.update_current_game()
|
217
|
+
if use_prev_audio:
|
218
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card), reuse_audio=True)
|
219
|
+
else:
|
220
|
+
print("New card(s) detected!")
|
221
|
+
obs.save_replay_buffer()
|
222
|
+
|
223
|
+
|
224
|
+
def check_tags_for_should_update(last_card):
|
225
|
+
if get_config().anki.tags_to_check:
|
226
|
+
found = False
|
227
|
+
for tag in last_card['tags']:
|
228
|
+
logger.info(tag)
|
229
|
+
logger.info(get_config().anki.tags_to_check)
|
230
|
+
if tag.lower() in get_config().anki.tags_to_check:
|
231
|
+
found = True
|
232
|
+
break
|
233
|
+
return found
|
234
|
+
else:
|
235
|
+
return True
|
236
|
+
|
237
|
+
|
238
|
+
# Main function to handle the script lifecycle
|
239
|
+
def monitor_anki():
|
240
|
+
try:
|
241
|
+
# Continuously check for new cards
|
242
|
+
while True:
|
243
|
+
check_for_new_cards()
|
244
|
+
time.sleep(get_config().anki.polling_rate / 1000.0) # Check every 200ms
|
245
|
+
except KeyboardInterrupt:
|
246
|
+
print("Stopped Checking For Anki Cards...")
|
247
|
+
|
248
|
+
|
249
|
+
# Fetch recent note IDs from Anki
|
250
|
+
def get_note_ids():
|
251
|
+
response = req.post(get_config().anki.url, json={
|
252
|
+
"action": "findNotes",
|
253
|
+
"version": 6,
|
254
|
+
"params": {"query": "added:1"}
|
255
|
+
})
|
256
|
+
result = response.json()
|
257
|
+
return set(result['result'])
|
258
|
+
|
259
|
+
|
260
|
+
def start_monitoring_anki():
|
261
|
+
# Start monitoring anki
|
262
|
+
if get_config().obs.enabled and get_config().features.full_auto:
|
263
|
+
obs_thread = threading.Thread(target=monitor_anki)
|
264
|
+
obs_thread.daemon = True # Ensures the thread will exit when the main program exits
|
265
|
+
obs_thread.start()
|