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.
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()