GameSentenceMiner 2.9.3__py3-none-any.whl → 2.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +3 -3
  2. GameSentenceMiner/anki.py +17 -11
  3. GameSentenceMiner/assets/icon.png +0 -0
  4. GameSentenceMiner/assets/icon128.png +0 -0
  5. GameSentenceMiner/assets/icon256.png +0 -0
  6. GameSentenceMiner/assets/icon32.png +0 -0
  7. GameSentenceMiner/assets/icon512.png +0 -0
  8. GameSentenceMiner/assets/icon64.png +0 -0
  9. GameSentenceMiner/assets/pickaxe.png +0 -0
  10. GameSentenceMiner/config_gui.py +22 -7
  11. GameSentenceMiner/gametext.py +5 -5
  12. GameSentenceMiner/gsm.py +26 -67
  13. GameSentenceMiner/obs.py +7 -9
  14. GameSentenceMiner/ocr/owocr_area_selector.py +1 -1
  15. GameSentenceMiner/ocr/owocr_helper.py +30 -13
  16. GameSentenceMiner/owocr/owocr/ocr.py +0 -2
  17. GameSentenceMiner/owocr/owocr/run.py +1 -1
  18. GameSentenceMiner/{communication → util/communication}/__init__.py +1 -1
  19. GameSentenceMiner/{communication → util/communication}/send.py +1 -1
  20. GameSentenceMiner/{communication → util/communication}/websocket.py +2 -2
  21. GameSentenceMiner/{downloader → util/downloader}/download_tools.py +3 -3
  22. GameSentenceMiner/vad.py +344 -0
  23. GameSentenceMiner/web/texthooking_page.py +78 -55
  24. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/METADATA +2 -3
  25. gamesentenceminer-2.9.5.dist-info/RECORD +57 -0
  26. GameSentenceMiner/configuration.py +0 -647
  27. GameSentenceMiner/electron_config.py +0 -315
  28. GameSentenceMiner/ffmpeg.py +0 -441
  29. GameSentenceMiner/model.py +0 -177
  30. GameSentenceMiner/notification.py +0 -105
  31. GameSentenceMiner/package.py +0 -39
  32. GameSentenceMiner/ss_selector.py +0 -121
  33. GameSentenceMiner/text_log.py +0 -186
  34. GameSentenceMiner/util.py +0 -262
  35. GameSentenceMiner/vad/groq_trim.py +0 -82
  36. GameSentenceMiner/vad/result.py +0 -21
  37. GameSentenceMiner/vad/silero_trim.py +0 -52
  38. GameSentenceMiner/vad/vad_utils.py +0 -13
  39. GameSentenceMiner/vad/vosk_helper.py +0 -158
  40. GameSentenceMiner/vad/whisper_helper.py +0 -105
  41. gamesentenceminer-2.9.3.dist-info/RECORD +0 -64
  42. /GameSentenceMiner/{downloader → assets}/__init__.py +0 -0
  43. /GameSentenceMiner/{downloader → util/downloader}/Untitled_json.py +0 -0
  44. /GameSentenceMiner/{vad → util/downloader}/__init__.py +0 -0
  45. /GameSentenceMiner/{downloader → util/downloader}/oneocr_dl.py +0 -0
  46. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/WHEEL +0 -0
  47. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/entry_points.txt +0 -0
  48. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/licenses/LICENSE +0 -0
  49. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/top_level.txt +0 -0
@@ -1,441 +0,0 @@
1
- import shutil
2
- import tempfile
3
-
4
- import GameSentenceMiner.configuration
5
- from GameSentenceMiner import obs, util, configuration
6
- from GameSentenceMiner.configuration import *
7
- from GameSentenceMiner.text_log import initial_time
8
- from GameSentenceMiner.util import *
9
-
10
-
11
- def get_ffmpeg_path():
12
- return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
13
-
14
- def get_ffprobe_path():
15
- return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
16
-
17
- ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
18
-
19
-
20
- def call_frame_extractor(video_path, timestamp):
21
- """
22
- Calls the video frame extractor script and captures the output.
23
-
24
- Args:
25
- video_path (str): Path to the video file.
26
- timestamp (str): Timestamp string (HH:MM:SS).
27
-
28
- Returns:
29
- str: The path of the selected image, or None on error.
30
- """
31
- try:
32
- # Get the directory of the current script
33
- current_dir = os.path.dirname(os.path.abspath(__file__))
34
- # Construct the path to the frame extractor script
35
- script_path = os.path.join(current_dir, "ss_selector.py") # Replace with the actual script name if different
36
-
37
- logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp)]))
38
-
39
- # Run the script using subprocess.run()
40
- result = subprocess.run(
41
- [sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
42
- capture_output=True,
43
- text=True, # Get output as text
44
- check=False # Raise an exception for non-zero exit codes
45
- )
46
- if result.returncode != 0:
47
- logger.error(f"Script failed with return code: {result.returncode}")
48
- return None
49
- logger.info(result)
50
- # Print the standard output
51
- logger.info(f"Frame extractor script output: {result.stdout.strip()}")
52
- return result.stdout.strip() # Return the output
53
-
54
- except subprocess.CalledProcessError as e:
55
- logger.error(f"Error calling script: {e}")
56
- logger.error(f"Script output (stderr): {e.stderr.strip()}")
57
- return None
58
- except FileNotFoundError:
59
- logger.error(f"Error: Script not found at {script_path}. Make sure the script name is correct.")
60
- return None
61
- except Exception as e:
62
- logger.error(f"An unexpected error occurred: {e}")
63
- return None
64
-
65
- def get_screenshot(video_file, screenshot_timing, try_selector=False):
66
- screenshot_timing = screenshot_timing if screenshot_timing else 1
67
- if try_selector:
68
- filepath = call_frame_extractor(video_path=video_file, timestamp=screenshot_timing)
69
- output = process_image(filepath)
70
- if output:
71
- return output
72
- else:
73
- logger.error("Frame extractor script failed to run or returned no output, defaulting")
74
- output_image = make_unique_file_name(os.path.join(
75
- get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
76
- # FFmpeg command to extract the last frame of the video
77
- ffmpeg_command = ffmpeg_base_command_list + [
78
- "-ss", f"{screenshot_timing}",
79
- "-i", f"{video_file}",
80
- "-vframes", "1" # Extract only one frame
81
- ]
82
-
83
- if get_config().screenshot.custom_ffmpeg_settings:
84
- ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.replace("\"", "").split(" "))
85
- else:
86
- ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
87
-
88
- if get_config().screenshot.width or get_config().screenshot.height:
89
- ffmpeg_command.extend(
90
- ["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
91
-
92
- ffmpeg_command.append(f"{output_image}")
93
-
94
- logger.debug(f"FFMPEG SS Command: {ffmpeg_command}")
95
-
96
- try:
97
- for i in range(3):
98
- logger.debug(" ".join(ffmpeg_command))
99
- result = subprocess.run(ffmpeg_command)
100
- if result.returncode != 0 and i < 2:
101
- raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
102
- else:
103
- break
104
- except Exception as e:
105
- logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
106
- output_image = make_unique_file_name(os.path.join(
107
- get_config().paths.screenshot_destination,
108
- f"{obs.get_current_game(sanitize=True)}.png"))
109
- ffmpeg_command = ffmpeg_base_command_list + [
110
- "-ss", f"{screenshot_timing}", # Default to 1 second
111
- "-i", video_file,
112
- "-vframes", "1",
113
- output_image
114
- ]
115
- subprocess.run(ffmpeg_command)
116
-
117
- logger.debug(f"Screenshot saved to: {output_image}")
118
-
119
- return output_image
120
-
121
- def get_screenshot_for_line(video_file, game_line, try_selector=False):
122
- return get_screenshot(video_file, get_screenshot_time(video_file, game_line), try_selector)
123
-
124
-
125
- def get_screenshot_time(video_path, game_line, default_beginning=False, vad_result=None, doing_multi_line=False, previous_line=False):
126
- if game_line:
127
- line_time = game_line.time
128
- else:
129
- # Assuming initial_time is defined elsewhere if game_line is None
130
- line_time = initial_time
131
- if previous_line:
132
- logger.debug(f"Calculating screenshot time for previous line: {str(game_line.text)}")
133
- else:
134
- logger.debug("Calculating screenshot time for line: " + str(game_line.text))
135
-
136
- file_length = get_video_duration(video_path)
137
- file_mod_time = get_file_modification_time(video_path)
138
-
139
- # Calculate when the line occurred within the video file (seconds from start)
140
- time_delta = file_mod_time - line_time
141
- line_timestamp_in_video = file_length - time_delta.total_seconds()
142
- screenshot_offset = get_config().screenshot.seconds_after_line
143
-
144
- # Calculate screenshot time from the beginning by adding the offset
145
- # if vad_result and vad_result.success and not doing_multi_line:
146
- # screenshot_time_from_beginning = line_timestamp_in_video + vad_result.end - 1
147
- # logger.info(f"Using VAD result {vad_result} for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
148
- if get_config().screenshot.screenshot_timing_setting == "beginning":
149
- screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
150
- logger.debug(f"Using 'beginning' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
151
- elif get_config().screenshot.screenshot_timing_setting == "middle":
152
- if game_line.next:
153
- screenshot_time_from_beginning = line_timestamp_in_video + ((game_line.next.time - game_line.time).total_seconds() / 2) + screenshot_offset
154
- else:
155
- screenshot_time_from_beginning = (file_length - ((file_length - line_timestamp_in_video) / 2)) + screenshot_offset
156
- logger.debug(f"Using 'middle' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
157
- elif get_config().screenshot.screenshot_timing_setting == "end":
158
- if game_line.next:
159
- screenshot_time_from_beginning = line_timestamp_in_video + (game_line.next.time - game_line.time).total_seconds() - screenshot_offset
160
- else:
161
- screenshot_time_from_beginning = file_length - screenshot_offset
162
- logger.debug(f"Using 'end' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
163
- else:
164
- logger.error(f"Invalid screenshot timing setting: {get_config().screenshot.screenshot_timing_setting}")
165
- screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
166
-
167
- # Check if the calculated time is out of bounds
168
- if screenshot_time_from_beginning < 0 or screenshot_time_from_beginning > file_length:
169
- logger.error(
170
- f"Calculated screenshot time ({screenshot_time_from_beginning:.2f}s) is out of bounds for video (length {file_length:.2f}s)."
171
- )
172
- if default_beginning:
173
- return 1.0
174
- return file_length - screenshot_offset
175
-
176
- # Return the calculated time from the beginning
177
- return screenshot_time_from_beginning
178
-
179
-
180
- def process_image(image_file):
181
- output_image = make_unique_file_name(
182
- os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
183
-
184
- # FFmpeg command to process the input image
185
- ffmpeg_command = ffmpeg_base_command_list + [
186
- "-i", image_file
187
- ]
188
-
189
- if get_config().screenshot.custom_ffmpeg_settings:
190
- ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.split(" "))
191
- else:
192
- ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
193
-
194
- if get_config().screenshot.width or get_config().screenshot.height:
195
- ffmpeg_command.extend(
196
- ["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
197
-
198
- ffmpeg_command.append(output_image)
199
- logger.debug(ffmpeg_command)
200
- logger.debug(" ".join(ffmpeg_command))
201
- # Run the command
202
- try:
203
- for i in range(3):
204
- logger.debug(" ".join(ffmpeg_command))
205
- result = subprocess.run(ffmpeg_command)
206
- if result.returncode != 0 and i < 2:
207
- raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
208
- else:
209
- break
210
- except Exception as e:
211
- logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
212
- output_image = make_unique_file_name(os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.png"))
213
- shutil.move(image_file, output_image)
214
-
215
- logger.info(f"Processed image saved to: {output_image}")
216
-
217
- return output_image
218
-
219
-
220
- def get_audio_codec(video_path):
221
- command = [
222
- f"{get_ffprobe_path()}",
223
- "-v", "error",
224
- "-select_streams", "a:0",
225
- "-show_entries", "stream=codec_name",
226
- "-of", "json",
227
- video_path
228
- ]
229
-
230
- logger.debug(" ".join(command))
231
- # Run the command and capture the output
232
- result = subprocess.run(command, capture_output=True, text=True)
233
-
234
- # Parse the JSON output
235
- try:
236
- output = json.loads(result.stdout)
237
- codec_name = output['streams'][0]['codec_name']
238
- return codec_name
239
- except (json.JSONDecodeError, KeyError, IndexError):
240
- logger.error("Failed to get codec information. Re-encoding Anyways")
241
- return None
242
-
243
-
244
- def get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time):
245
- supported_formats = {
246
- 'opus': 'libopus',
247
- 'mp3': 'libmp3lame',
248
- 'ogg': 'libvorbis',
249
- 'aac': 'aac',
250
- 'm4a': 'aac',
251
- }
252
-
253
- codec = get_audio_codec(video_path)
254
-
255
- if codec == get_config().audio.extension:
256
- codec_command = ['-c:a', 'copy']
257
- logger.debug(f"Extracting {get_config().audio.extension} from video")
258
- else:
259
- codec_command = ["-c:a", f"{supported_formats[get_config().audio.extension]}"]
260
- logger.debug(f"Re-encoding {codec} to {get_config().audio.extension}")
261
-
262
- untrimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
263
- suffix=f"_untrimmed.{get_config().audio.extension}").name
264
-
265
- command = ffmpeg_base_command_list + [
266
- "-i", video_path,
267
- "-map", "0:a"] + codec_command + [
268
- untrimmed_audio
269
- ]
270
-
271
- # FFmpeg command to extract OR re-encode the audio
272
- # command = f"{ffmpeg_base_command} -i \"{video_path}\" -map 0:a {codec_command} \"{untrimmed_audio}\""
273
- logger.debug("Doing initial audio extraction")
274
- logger.debug(" ".join(command))
275
-
276
- subprocess.run(command)
277
-
278
- return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time, anki_card_creation_time)
279
-
280
-
281
- def get_video_duration(file_path):
282
- ffprobe_command = [
283
- f"{get_ffprobe_path()}",
284
- "-v", "error",
285
- "-show_entries", "format=duration",
286
- "-of", "json",
287
- file_path
288
- ]
289
- logger.debug(" ".join(ffprobe_command))
290
- result = subprocess.run(ffprobe_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
291
- duration_info = json.loads(result.stdout)
292
- logger.debug(f"Video duration: {duration_info}")
293
- return float(duration_info["format"]["duration"]) # Return the duration in seconds
294
-
295
-
296
- def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line, anki_card_creation_time):
297
- trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
298
- suffix=f".{get_config().audio.extension}").name
299
- start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line, anki_card_creation_time)
300
-
301
- ffmpeg_command = ffmpeg_base_command_list + [
302
- "-i", untrimmed_audio,
303
- "-ss", str(start_trim_time)]
304
- if next_line and next_line > game_line.time:
305
- end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
306
- hours, remainder = divmod(end_total_seconds, 3600)
307
- minutes, seconds = divmod(remainder, 60)
308
- end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
309
- ffmpeg_command.extend(['-to', end_trim_time])
310
- logger.debug(
311
- f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time}")
312
-
313
- ffmpeg_command.extend([
314
- "-c", "copy", # Using copy to avoid re-encoding, adjust if needed
315
- trimmed_audio
316
- ])
317
-
318
- logger.debug(" ".join(ffmpeg_command))
319
- subprocess.run(ffmpeg_command)
320
-
321
- logger.debug(f"{total_seconds_after_offset} trimmed off of beginning")
322
-
323
- logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
324
- return trimmed_audio
325
-
326
- def get_video_timings(video_path, game_line, anki_card_creation_time=None):
327
- if anki_card_creation_time and get_config().advanced.use_anki_note_creation_time:
328
- file_mod_time = anki_card_creation_time
329
- else:
330
- file_mod_time = get_file_modification_time(video_path)
331
- file_length = get_video_duration(video_path)
332
- time_delta = file_mod_time - game_line.time
333
- # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
334
- total_seconds = file_length - time_delta.total_seconds()
335
- total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
336
- if total_seconds < 0 or total_seconds >= file_length:
337
- logger.error("Line mined is outside of the replay buffer! Defaulting to the beginning of the replay buffer. ")
338
- logger.info("Recommend either increasing replay buffer length in OBS Settings or mining faster.")
339
- return 0, 0, 0
340
-
341
- hours, remainder = divmod(total_seconds_after_offset, 3600)
342
- minutes, seconds = divmod(remainder, 60)
343
- start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
344
- return start_trim_time, total_seconds, total_seconds_after_offset
345
-
346
-
347
- def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
348
- logger.debug(f"Re-encode running with settings: {user_ffmpeg_options}")
349
- temp_file = create_temp_file_with_same_name(input_file)
350
- command = ffmpeg_base_command_list + [
351
- "-i", input_file,
352
- "-map", "0:a"
353
- ] + user_ffmpeg_options.replace("\"", "").split(" ") + [
354
- temp_file
355
- ]
356
-
357
- logger.debug(" ".join(command))
358
- process = subprocess.run(command)
359
-
360
- if process.returncode != 0:
361
- logger.error("Re-encode failed, using original audio")
362
- return
363
-
364
- replace_file_with_retry(temp_file, final_output_audio)
365
-
366
-
367
- def create_temp_file_with_same_name(input_file: str):
368
- split = input_file.split(".")
369
- return f"{split[0]}_temp.{split[1]}"
370
-
371
-
372
- def replace_file_with_retry(temp_file, input_file, retries=5, delay=1):
373
- for attempt in range(retries):
374
- try:
375
- shutil.move(temp_file, input_file)
376
- return
377
- except OSError as e:
378
- if attempt < retries - 1:
379
- logger.warning(f"Attempt {attempt + 1}: File still in use. Retrying in {delay} seconds...")
380
- time.sleep(delay)
381
- else:
382
- logger.error(f"Failed to replace the file after {retries} attempts. Error: {e}")
383
- raise
384
-
385
-
386
- def trim_audio_by_end_time(input_audio, end_time, output_audio):
387
- command = ffmpeg_base_command_list + [
388
- "-i", input_audio,
389
- "-to", str(end_time),
390
- "-c", "copy",
391
- output_audio
392
- ]
393
- logger.debug(" ".join(command))
394
- subprocess.run(command)
395
-
396
-
397
- def convert_audio_to_wav(input_audio, output_wav):
398
- command = ffmpeg_base_command_list + [
399
- "-i", input_audio,
400
- "-ar", "16000", # Resample to 16kHz
401
- "-ac", "1", # Convert to mono
402
- "-af", "afftdn,dialoguenhance" if not is_linux() else "afftdn",
403
- output_wav
404
- ]
405
- logger.debug(" ".join(command))
406
- subprocess.run(command)
407
-
408
-
409
- # Trim the audio using FFmpeg based on detected speech timestamps
410
- def trim_audio(input_audio, start_time, end_time, output_audio):
411
- command = ffmpeg_base_command_list.copy()
412
-
413
- command.extend(['-i', input_audio])
414
-
415
- if get_config().vad.trim_beginning and start_time > 0:
416
- logger.debug(f"trimming beginning to {start_time}")
417
- command.extend(['-ss', f"{start_time:.2f}"])
418
-
419
- command.extend([
420
- '-to', f"{end_time:.2f}",
421
- '-c', 'copy',
422
- output_audio
423
- ])
424
-
425
- logger.debug(" ".join(command))
426
-
427
- subprocess.run(command)
428
-
429
-
430
- def is_video_big_enough(file_path, min_size_kb=250):
431
- try:
432
- file_size = os.path.getsize(file_path) # Size in bytes
433
- file_size_kb = file_size / 1024 # Convert to KB
434
- return file_size_kb >= min_size_kb
435
- except FileNotFoundError:
436
- logger.error("File not found!")
437
- return False
438
- except Exception as e:
439
- logger.error(f"Error: {e}")
440
- return False
441
-
@@ -1,177 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional, List
3
-
4
- from dataclasses_json import dataclass_json
5
-
6
- from GameSentenceMiner.configuration import get_config, logger, save_current_config
7
-
8
-
9
- # OBS
10
- @dataclass_json
11
- @dataclass
12
- class SceneInfo:
13
- currentProgramSceneName: str
14
- currentProgramSceneUuid: str
15
- sceneName: str
16
- sceneUuid: str
17
-
18
-
19
- @dataclass_json
20
- @dataclass
21
- class SceneItemTransform:
22
- alignment: int
23
- boundsAlignment: int
24
- boundsHeight: float
25
- boundsType: str
26
- boundsWidth: float
27
- cropBottom: int
28
- cropLeft: int
29
- cropRight: int
30
- cropToBounds: bool
31
- cropTop: int
32
- height: float
33
- positionX: float
34
- positionY: float
35
- rotation: float
36
- scaleX: float
37
- scaleY: float
38
- sourceHeight: float
39
- sourceWidth: float
40
- width: float
41
-
42
-
43
- @dataclass_json
44
- @dataclass
45
- class SceneItem:
46
- inputKind: str
47
- isGroup: Optional[bool]
48
- sceneItemBlendMode: str
49
- sceneItemEnabled: bool
50
- sceneItemId: int
51
- sceneItemIndex: int
52
- sceneItemLocked: bool
53
- sceneItemTransform: SceneItemTransform
54
- sourceName: str
55
- sourceType: str
56
- sourceUuid: str
57
-
58
- # def __init__(self, **kwargs):
59
- # self.inputKind = kwargs['inputKind']
60
- # self.isGroup = kwargs['isGroup']
61
- # self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
62
- # self.sceneItemEnabled = kwargs['sceneItemEnabled']
63
- # self.sceneItemId = kwargs['sceneItemId']
64
- # self.sceneItemIndex = kwargs['sceneItemIndex']
65
- # self.sceneItemLocked = kwargs['sceneItemLocked']
66
- # self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
67
- # self.sourceName = kwargs['sourceName']
68
- # self.sourceType = kwargs['sourceType']
69
- # self.sourceUuid = kwargs['sourceUuid']
70
-
71
-
72
- @dataclass_json
73
- @dataclass
74
- class SceneItemsResponse:
75
- sceneItems: List[SceneItem]
76
-
77
- # def __init__(self, **kwargs):
78
- # self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
79
-
80
-
81
- @dataclass_json
82
- @dataclass
83
- class RecordDirectory:
84
- recordDirectory: str
85
-
86
-
87
- @dataclass_json
88
- @dataclass
89
- class SceneItemInfo:
90
- sceneIndex: int
91
- sceneName: str
92
- sceneUuid: str
93
-
94
-
95
- @dataclass_json
96
- @dataclass
97
- class SceneListResponse:
98
- scenes: List[SceneItemInfo]
99
- currentProgramSceneName: Optional[str] = None
100
- currentProgramSceneUuid: Optional[str] = None
101
- currentPreviewSceneName: Optional[str] = None
102
- currentPreviewSceneUuid: Optional[str] = None
103
-
104
- #
105
- # @dataclass_json
106
- # @dataclass
107
- # class SourceActive:
108
- # videoActive: bool
109
- # videoShowing: bool
110
-
111
- @dataclass_json
112
- @dataclass
113
- class AnkiCard:
114
- noteId: int
115
- tags: list[str]
116
- fields: dict[str, dict[str, str]]
117
- cards: list[int]
118
- alternatives = {
119
- "word_field": ["Front", "Word", "TargetWord", "Expression"],
120
- "sentence_field": ["Example", "Context", "Back", "Sentence"],
121
- "picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
122
- "sentence_audio_field": ["SentenceAudio"]
123
- }
124
-
125
- def get_field(self, field_name: str) -> str:
126
- if self.has_field(field_name):
127
- return self.fields[field_name]['value']
128
- else:
129
- raise ValueError(f"Field '{field_name}' not found in AnkiCard. Please make sure your Anki Field Settings in GSM Match your fields in your Anki Note!")
130
-
131
- def has_field (self, field_name: str) -> bool:
132
- return field_name in self.fields
133
-
134
- def __post_init__(self):
135
- config = get_config()
136
- changes_found = False
137
- if not self.has_field(config.anki.word_field):
138
- found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
139
- if found_alternative_field:
140
- logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
141
- config.anki.word_field = field
142
- changes_found = True
143
-
144
- if not self.has_field(config.anki.sentence_field):
145
- found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
146
- if found_alternative_field:
147
- logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
148
- config.anki.sentence_field = field
149
- changes_found = True
150
-
151
- if not self.has_field(config.anki.picture_field):
152
- found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
153
- if found_alternative_field:
154
- logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
155
- config.anki.picture_field = field
156
- changes_found = True
157
-
158
- if not self.has_field(config.anki.sentence_audio_field):
159
- found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
160
- if found_alternative_field:
161
- logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
162
- config.anki.sentence_audio_field = field
163
- changes_found = True
164
-
165
- if changes_found:
166
- save_current_config(config)
167
-
168
- def find_field(self, field, field_type):
169
- if field in self.fields:
170
- return False, field
171
-
172
- for alt_field in self.alternatives[field_type]:
173
- for key in self.fields:
174
- if alt_field.lower() == key.lower():
175
- return True, key
176
-
177
- return False, None