GameSentenceMiner 2.14.8__py3-none-any.whl → 2.14.9__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 (66) hide show
  1. GameSentenceMiner/config_gui.py +19 -10
  2. GameSentenceMiner/gsm.py +68 -8
  3. GameSentenceMiner/locales/en_us.json +4 -0
  4. GameSentenceMiner/locales/ja_jp.json +4 -0
  5. GameSentenceMiner/locales/zh_cn.json +4 -0
  6. GameSentenceMiner/obs.py +12 -8
  7. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/METADATA +1 -2
  8. gamesentenceminer-2.14.9.dist-info/RECORD +24 -0
  9. GameSentenceMiner/ai/__init__.py +0 -0
  10. GameSentenceMiner/ai/ai_prompting.py +0 -473
  11. GameSentenceMiner/ocr/__init__.py +0 -0
  12. GameSentenceMiner/ocr/gsm_ocr_config.py +0 -174
  13. GameSentenceMiner/ocr/ocrconfig.py +0 -129
  14. GameSentenceMiner/ocr/owocr_area_selector.py +0 -629
  15. GameSentenceMiner/ocr/owocr_helper.py +0 -638
  16. GameSentenceMiner/ocr/ss_picker.py +0 -140
  17. GameSentenceMiner/owocr/owocr/__init__.py +0 -1
  18. GameSentenceMiner/owocr/owocr/__main__.py +0 -9
  19. GameSentenceMiner/owocr/owocr/config.py +0 -148
  20. GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -1238
  21. GameSentenceMiner/owocr/owocr/ocr.py +0 -1690
  22. GameSentenceMiner/owocr/owocr/run.py +0 -1817
  23. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -109
  24. GameSentenceMiner/tools/__init__.py +0 -0
  25. GameSentenceMiner/tools/audio_offset_selector.py +0 -215
  26. GameSentenceMiner/tools/ss_selector.py +0 -135
  27. GameSentenceMiner/tools/window_transparency.py +0 -214
  28. GameSentenceMiner/util/__init__.py +0 -0
  29. GameSentenceMiner/util/communication/__init__.py +0 -22
  30. GameSentenceMiner/util/communication/send.py +0 -7
  31. GameSentenceMiner/util/communication/websocket.py +0 -94
  32. GameSentenceMiner/util/configuration.py +0 -1198
  33. GameSentenceMiner/util/db.py +0 -408
  34. GameSentenceMiner/util/downloader/Untitled_json.py +0 -472
  35. GameSentenceMiner/util/downloader/__init__.py +0 -0
  36. GameSentenceMiner/util/downloader/download_tools.py +0 -194
  37. GameSentenceMiner/util/downloader/oneocr_dl.py +0 -250
  38. GameSentenceMiner/util/electron_config.py +0 -259
  39. GameSentenceMiner/util/ffmpeg.py +0 -571
  40. GameSentenceMiner/util/get_overlay_coords.py +0 -366
  41. GameSentenceMiner/util/gsm_utils.py +0 -323
  42. GameSentenceMiner/util/model.py +0 -206
  43. GameSentenceMiner/util/notification.py +0 -147
  44. GameSentenceMiner/util/text_log.py +0 -214
  45. GameSentenceMiner/web/__init__.py +0 -0
  46. GameSentenceMiner/web/service.py +0 -132
  47. GameSentenceMiner/web/static/__init__.py +0 -0
  48. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  49. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  50. GameSentenceMiner/web/static/favicon.ico +0 -0
  51. GameSentenceMiner/web/static/favicon.svg +0 -3
  52. GameSentenceMiner/web/static/site.webmanifest +0 -21
  53. GameSentenceMiner/web/static/style.css +0 -292
  54. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  55. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  56. GameSentenceMiner/web/templates/__init__.py +0 -0
  57. GameSentenceMiner/web/templates/index.html +0 -50
  58. GameSentenceMiner/web/templates/text_replacements.html +0 -238
  59. GameSentenceMiner/web/templates/utility.html +0 -483
  60. GameSentenceMiner/web/texthooking_page.py +0 -584
  61. GameSentenceMiner/wip/__init___.py +0 -0
  62. gamesentenceminer-2.14.8.dist-info/RECORD +0 -77
  63. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/WHEEL +0 -0
  64. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/entry_points.txt +0 -0
  65. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/licenses/LICENSE +0 -0
  66. {gamesentenceminer-2.14.8.dist-info → gamesentenceminer-2.14.9.dist-info}/top_level.txt +0 -0
@@ -1,206 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional, List
3
- from enum import Enum
4
-
5
- from dataclasses_json import dataclass_json
6
-
7
- from GameSentenceMiner.util.configuration import get_config, logger, save_current_config
8
-
9
-
10
- # OBS
11
- @dataclass_json
12
- @dataclass
13
- class SceneInfo:
14
- currentProgramSceneName: str
15
- currentProgramSceneUuid: str
16
- sceneName: str
17
- sceneUuid: str
18
-
19
-
20
- @dataclass_json
21
- @dataclass
22
- class SceneItemTransform:
23
- alignment: int
24
- boundsAlignment: int
25
- boundsHeight: float
26
- boundsType: str
27
- boundsWidth: float
28
- cropBottom: int
29
- cropLeft: int
30
- cropRight: int
31
- cropToBounds: bool
32
- cropTop: int
33
- height: float
34
- positionX: float
35
- positionY: float
36
- rotation: float
37
- scaleX: float
38
- scaleY: float
39
- sourceHeight: float
40
- sourceWidth: float
41
- width: float
42
-
43
-
44
- @dataclass_json
45
- @dataclass
46
- class SceneItem:
47
- inputKind: str
48
- isGroup: Optional[bool]
49
- sceneItemBlendMode: str
50
- sceneItemEnabled: bool
51
- sceneItemId: int
52
- sceneItemIndex: int
53
- sceneItemLocked: bool
54
- sceneItemTransform: SceneItemTransform
55
- sourceName: str
56
- sourceType: str
57
- sourceUuid: str
58
-
59
- # def __init__(self, **kwargs):
60
- # self.inputKind = kwargs['inputKind']
61
- # self.isGroup = kwargs['isGroup']
62
- # self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
63
- # self.sceneItemEnabled = kwargs['sceneItemEnabled']
64
- # self.sceneItemId = kwargs['sceneItemId']
65
- # self.sceneItemIndex = kwargs['sceneItemIndex']
66
- # self.sceneItemLocked = kwargs['sceneItemLocked']
67
- # self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
68
- # self.sourceName = kwargs['sourceName']
69
- # self.sourceType = kwargs['sourceType']
70
- # self.sourceUuid = kwargs['sourceUuid']
71
-
72
-
73
- @dataclass_json
74
- @dataclass
75
- class SceneItemsResponse:
76
- sceneItems: List[SceneItem]
77
-
78
- # def __init__(self, **kwargs):
79
- # self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
80
-
81
-
82
- @dataclass_json
83
- @dataclass
84
- class RecordDirectory:
85
- recordDirectory: str
86
-
87
-
88
- @dataclass_json
89
- @dataclass
90
- class SceneItemInfo:
91
- sceneIndex: int
92
- sceneName: str
93
- sceneUuid: str
94
-
95
-
96
- @dataclass_json
97
- @dataclass
98
- class SceneListResponse:
99
- scenes: List[SceneItemInfo]
100
- currentProgramSceneName: Optional[str] = None
101
- currentProgramSceneUuid: Optional[str] = None
102
- currentPreviewSceneName: Optional[str] = None
103
- currentPreviewSceneUuid: Optional[str] = None
104
-
105
- #
106
- # @dataclass_json
107
- # @dataclass
108
- # class SourceActive:
109
- # videoActive: bool
110
- # videoShowing: bool
111
-
112
- @dataclass_json
113
- @dataclass
114
- class AnkiField:
115
- value: str
116
- order: int
117
-
118
- @dataclass_json
119
- @dataclass
120
- class AnkiCard:
121
- noteId: int
122
- tags: list[str]
123
- fields: dict[str, AnkiField]
124
- cards: list[int]
125
- alternatives = {
126
- "word_field": ["Front", "Word", "TargetWord", "Expression"],
127
- "sentence_field": ["Example", "Context", "Back", "Sentence"],
128
- "picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
129
- "sentence_audio_field": ["SentenceAudio"]
130
- }
131
-
132
- def get_field(self, field_name: str) -> str:
133
- if self.has_field(field_name):
134
- return self.fields[field_name].value
135
- else:
136
- 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!")
137
-
138
- def has_field (self, field_name: str) -> bool:
139
- return field_name in self.fields
140
-
141
- def __post_init__(self):
142
- config = get_config()
143
- changes_found = False
144
- if not self.has_field(config.anki.word_field):
145
- found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
146
- if found_alternative_field:
147
- logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
148
- config.anki.word_field = field
149
- changes_found = True
150
-
151
- if not self.has_field(config.anki.sentence_field):
152
- found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
153
- if found_alternative_field:
154
- logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
155
- config.anki.sentence_field = field
156
- changes_found = True
157
-
158
- if not self.has_field(config.anki.picture_field):
159
- found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
160
- if found_alternative_field:
161
- logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
162
- config.anki.picture_field = field
163
- changes_found = True
164
-
165
- if not self.has_field(config.anki.sentence_audio_field):
166
- found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
167
- if found_alternative_field:
168
- logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
169
- config.anki.sentence_audio_field = field
170
- changes_found = True
171
-
172
- if changes_found:
173
- save_current_config(config)
174
-
175
- def find_field(self, field, field_type):
176
- if field in self.fields:
177
- return False, field
178
-
179
- for alt_field in self.alternatives[field_type]:
180
- for key in self.fields:
181
- if alt_field.lower() == key.lower():
182
- return True, key
183
-
184
- return False, None
185
-
186
-
187
- class VADResult:
188
- def __init__(self, success: bool, start: float, end: float, model: str, segments: list = None, output_audio: str = None):
189
- self.success = success
190
- self.start = start
191
- self.end = end
192
- self.model = model
193
- self.segments = segments if segments is not None else []
194
- self.output_audio = output_audio
195
-
196
- def __repr__(self):
197
- return f"VADResult(success={self.success}, start={self.start}, end={self.end}, model={self.model}, output_audio={self.output_audio})"
198
-
199
- def trim_successful_string(self):
200
- if self.success:
201
- if get_config().vad.trim_beginning:
202
- return f"Trimmed audio from {self.start:.2f} to {self.end:.2f} seconds using {self.model}."
203
- else:
204
- return f"Trimmed end of audio to {self.end:.2f} seconds using {self.model}."
205
- else:
206
- return f"Failed to trim audio using {self.model}."
@@ -1,147 +0,0 @@
1
- import requests
2
- from plyer import notification
3
- from GameSentenceMiner.util.configuration import logger, is_windows
4
-
5
- if is_windows():
6
- from win10toast import ToastNotifier
7
-
8
- if is_windows():
9
- class MyToastNotifier(ToastNotifier):
10
- def __init__(self):
11
- super().__init__()
12
-
13
- def on_destroy(self, hwnd, msg, wparam, lparam):
14
- super().on_destroy(hwnd, msg, wparam, lparam)
15
- return 0
16
-
17
- if is_windows():
18
- notifier = MyToastNotifier()
19
- else:
20
- notifier = notification
21
-
22
-
23
- def open_browser_window(note_id, query=None):
24
- url = "http://localhost:8765"
25
- headers = {'Content-Type': 'application/json'}
26
-
27
- data = {
28
- "action": "guiBrowse",
29
- "version": 6,
30
- "params": {
31
- "query": f"nid:{note_id}" if not query else query,
32
- }
33
- }
34
-
35
- try:
36
- if query:
37
- blank_req_data = {
38
- "action": "guiBrowse",
39
- "version": 6,
40
- "params": {
41
- "query": "nid:1",
42
- }
43
- }
44
- requests.post(url, json=blank_req_data, headers=headers)
45
- response = requests.post(url, json=data, headers=headers)
46
- if response.status_code == 200:
47
- if query:
48
- logger.info(f"Opened Anki browser with query: {query}")
49
- else:
50
- logger.info(f"Opened Anki note in browser with ID {note_id}")
51
- else:
52
- logger.error(f"Failed to open Anki note with ID {note_id}")
53
- except Exception as e:
54
- logger.info(f"Error connecting to AnkiConnect: {e}")
55
-
56
-
57
- def open_anki_card(note_id):
58
- url = "http://localhost:8765"
59
- headers = {'Content-Type': 'application/json'}
60
-
61
- data = {
62
- "action": "guiEditNote",
63
- "version": 6,
64
- "params": {
65
- "note": note_id
66
- }
67
- }
68
-
69
- try:
70
- response = requests.post(url, json=data, headers=headers)
71
- if response.status_code == 200:
72
- logger.info(f"Opened Anki note with ID {note_id}")
73
- else:
74
- logger.error(f"Failed to open Anki note with ID {note_id}")
75
- except Exception as e:
76
- logger.info(f"Error connecting to AnkiConnect: {e}")
77
-
78
-
79
- def send_notification(title, message, timeout):
80
- try:
81
- if is_windows():
82
- notifier.show_toast(
83
- title, message, duration=timeout, threaded=True)
84
- else:
85
- notification.notify(
86
- title=title,
87
- message=message,
88
- app_name="GameSentenceMiner",
89
- timeout=timeout # Notification disappears after 5 seconds
90
- )
91
- except Exception as e:
92
- logger.error(f"Failed to send notification: {e}")
93
-
94
-
95
- def send_note_updated(tango):
96
- send_notification(
97
- title="Anki Card Updated",
98
- message=f"Audio and/or Screenshot added to note: {tango}",
99
- timeout=5 # Notification disappears after 5 seconds
100
- )
101
-
102
-
103
- def send_screenshot_updated(tango):
104
- send_notification(
105
- title="Anki Card Updated",
106
- message=f"Screenshot updated on note: {tango}",
107
- timeout=5 # Notification disappears after 5 seconds
108
- )
109
-
110
-
111
- def send_screenshot_saved(path):
112
- send_notification(
113
- title="Screenshot Saved",
114
- message=f"Screenshot saved to : {path}",
115
- timeout=5 # Notification disappears after 5 seconds
116
- )
117
-
118
-
119
- def send_audio_generated_notification(audio_path):
120
- send_notification(
121
- title="Audio Trimmed",
122
- message=f"Audio Trimmed and placed at {audio_path}",
123
- timeout=5 # Notification disappears after 5 seconds
124
- )
125
-
126
-
127
- def send_check_obs_notification(reason):
128
- send_notification(
129
- title="OBS Replay Invalid",
130
- message=f"Check OBS Settings! Reason: {reason}",
131
- timeout=5 # Notification disappears after 5 seconds
132
- )
133
-
134
-
135
- def send_error_no_anki_update():
136
- send_notification(
137
- title="Error",
138
- message=f"Anki Card not updated, Check Console for Reason!",
139
- timeout=5 # Notification disappears after 5 seconds
140
- )
141
-
142
- def send_error_notification(message):
143
- send_notification(
144
- title="Error",
145
- message=message,
146
- timeout=5 # Notification disappears after 5 seconds
147
- )
@@ -1,214 +0,0 @@
1
- import uuid
2
- from dataclasses import dataclass
3
- from datetime import datetime
4
- from difflib import SequenceMatcher
5
- from typing import Optional
6
-
7
- import rapidfuzz
8
-
9
- from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
10
- from GameSentenceMiner.util.configuration import logger, get_config, gsm_state
11
- from GameSentenceMiner.util.model import AnkiCard
12
- import re
13
-
14
- initial_time = datetime.now()
15
-
16
-
17
- @dataclass
18
- class GameLine:
19
- id: str
20
- text: str
21
- time: datetime
22
- prev: 'GameLine | None'
23
- next: 'GameLine | None'
24
- index: int = 0
25
- scene: str = ""
26
- TL: str = ""
27
-
28
- def get_previous_time(self):
29
- if self.prev:
30
- return self.prev.time
31
- return initial_time
32
-
33
- def get_next_time(self):
34
- if self.next:
35
- return self.next.time
36
- return 0
37
-
38
- def set_TL(self, tl: str):
39
- self.TL = tl
40
-
41
- def get_stripped_text(self):
42
- return self.text.replace('\n', '').strip()
43
-
44
- def __str__(self):
45
- return str({"text": self.text, "time": self.time})
46
-
47
-
48
- @dataclass
49
- class GameText:
50
- values: list[GameLine]
51
- values_dict: dict[str, GameLine]
52
- game_line_index = 0
53
-
54
- def __init__(self):
55
- self.values = []
56
- self.values_dict = {}
57
-
58
- def __getitem__(self, index):
59
- return self.values[index]
60
-
61
- def get_by_id(self, line_id: str) -> Optional[GameLine]:
62
- if not self.values_dict:
63
- return None
64
- return self.values_dict.get(line_id)
65
-
66
- def get_time(self, line_text: str, occurrence: int = -1) -> datetime:
67
- matches = [line for line in self.values if line.text == line_text]
68
- if matches:
69
- return matches[occurrence].time # Default to latest
70
- return initial_time
71
-
72
- def get_event(self, line_text: str, occurrence: int = -1) -> GameLine | None:
73
- matches = [line for line in self.values if line.text == line_text]
74
- if matches:
75
- return matches[occurrence]
76
- return None
77
-
78
- def add_line(self, line_text, line_time=None):
79
- if not line_text:
80
- return
81
- line_id = str(uuid.uuid1())
82
- new_line = GameLine(
83
- id=line_id, # Time-based UUID as an integer
84
- text=line_text,
85
- time=line_time or datetime.now(),
86
- prev=self.values[-1] if self.values else None,
87
- next=None,
88
- index=self.game_line_index,
89
- scene=gsm_state.current_game or ""
90
- )
91
- self.values_dict[line_id] = new_line
92
- logger.debug(f"Adding line: {new_line}")
93
- self.game_line_index += 1
94
- if self.values:
95
- self.values[-1].next = new_line
96
- self.values.append(new_line)
97
- # self.remove_old_events(datetime.now() - timedelta(minutes=10))
98
-
99
- def has_line(self, line_text) -> bool:
100
- for game_line in self.values:
101
- if game_line.text == line_text:
102
- return True
103
- return False
104
-
105
- def get_last_line(self):
106
- if self.values:
107
- return self.values[-1]
108
- return None
109
-
110
-
111
- game_log = GameText()
112
-
113
- def strip_whitespace_and_punctuation(text: str) -> str:
114
- """
115
- Strips whitespace and punctuation from the given text.
116
- """
117
- # Remove all whitespace and specified punctuation using regex
118
- # Includes Japanese and common punctuation
119
- return re.sub(r'[\s 、。「」【】《》., ]', '', text).strip()
120
-
121
-
122
- def lines_match(texthooker_sentence, anki_sentence):
123
- # Replace newlines, spaces, other whitespace characters, AND japanese punctuation
124
- texthooker_sentence = strip_whitespace_and_punctuation(texthooker_sentence)
125
- anki_sentence = strip_whitespace_and_punctuation(anki_sentence)
126
- similarity = rapidfuzz.fuzz.ratio(texthooker_sentence, anki_sentence)
127
- logger.debug(f"Comparing sentences: '{texthooker_sentence}' and '{anki_sentence}' - Similarity: {similarity}")
128
- if texthooker_sentence in anki_sentence:
129
- logger.debug(f"One contains the other: {texthooker_sentence} in {anki_sentence} - Similarity: {similarity}")
130
- elif anki_sentence in texthooker_sentence:
131
- logger.debug(f"One contains the other: {anki_sentence} in {texthooker_sentence} - Similarity: {similarity}")
132
- return (anki_sentence in texthooker_sentence) or (texthooker_sentence in anki_sentence) or (similarity >= 80)
133
-
134
-
135
- def get_text_event(last_note) -> GameLine:
136
- lines = game_log.values
137
-
138
- if not lines:
139
- raise Exception("No voicelines in GSM. GSM can only do work on text that has been sent to it since it started. If you are not getting any text into GSM, please check your setup/config.")
140
-
141
- if not last_note:
142
- return lines[-1]
143
-
144
- sentence = last_note.get_field(get_config().anki.sentence_field)
145
- if not sentence:
146
- return lines[-1]
147
-
148
- for line in reversed(lines):
149
- if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
150
- return line
151
-
152
- logger.info("Could not find matching sentence from GSM's history. Using the latest line.")
153
- return lines[-1]
154
-
155
-
156
- def get_line_and_future_lines(last_note):
157
- if not last_note:
158
- return []
159
-
160
- sentence = last_note.get_field(get_config().anki.sentence_field)
161
- found_lines = []
162
- if sentence:
163
- found = False
164
- for line in game_log.values:
165
- if found:
166
- found_lines.append(line)
167
- if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
168
- found = True
169
- found_lines.append(line)
170
- return found_lines
171
-
172
-
173
- def get_mined_line(last_note: AnkiCard, lines=None):
174
- if lines is None:
175
- lines = []
176
- if not last_note:
177
- return lines[-1]
178
- if not lines:
179
- lines = get_all_lines()
180
- if not lines:
181
- raise Exception("No voicelines in GSM. GSM can only do work on text that has been sent to it since it started. If you are not getting any text into GSM, please check your setup/config.")
182
-
183
- sentence = last_note.get_field(get_config().anki.sentence_field)
184
- for line in reversed(lines):
185
- if lines_match(line.get_stripped_text(), remove_html_and_cloze_tags(sentence)):
186
- return line
187
- return lines[-1]
188
-
189
-
190
- def get_time_of_line(line):
191
- return game_log.get_time(line)
192
-
193
-
194
- def get_all_lines():
195
- return game_log.values
196
-
197
-
198
- def get_text_log() -> GameText:
199
- return game_log
200
-
201
- def add_line(current_line_after_regex, line_time):
202
- game_log.add_line(current_line_after_regex, line_time)
203
-
204
- def get_line_by_id(line_id: str) -> Optional[GameLine]:
205
- """
206
- Retrieve a GameLine by its unique ID.
207
-
208
- Args:
209
- line_id (str): The unique identifier of the GameLine.
210
-
211
- Returns:
212
- Optional[GameLine]: The GameLine object if found, otherwise None.
213
- """
214
- return game_log.get_by_id(line_id)
File without changes
@@ -1,132 +0,0 @@
1
- import os
2
- import shutil
3
- import subprocess
4
- import threading
5
-
6
-
7
- from GameSentenceMiner import anki
8
- from GameSentenceMiner.util import ffmpeg, notification
9
- from GameSentenceMiner.util.configuration import gsm_state, logger, get_config, get_temporary_directory
10
- from GameSentenceMiner.util.ffmpeg import get_video_timings
11
- from GameSentenceMiner.util.text_log import GameLine
12
-
13
-
14
- def set_get_audio_from_video_callback(func):
15
- global get_audio_from_video
16
- get_audio_from_video = func
17
-
18
-
19
- def handle_texthooker_button(video_path=''):
20
- try:
21
- if gsm_state.line_for_audio:
22
- line: GameLine = gsm_state.line_for_audio
23
- gsm_state.line_for_audio = None
24
- if line == gsm_state.previous_line_for_audio:
25
- logger.info("Line is the same as the last one, skipping processing.")
26
- if get_config().advanced.audio_player_path:
27
- play_audio_in_external(gsm_state.previous_audio)
28
- elif get_config().advanced.video_player_path:
29
- play_video_in_external(line, video_path)
30
- else:
31
- import sounddevice as sd
32
- data, samplerate = gsm_state.previous_audio
33
- sd.play(data, samplerate)
34
- sd.wait()
35
- return
36
- gsm_state.previous_line_for_audio = line
37
- if get_config().advanced.audio_player_path:
38
- audio = get_audio_from_video(line, line.next.time if line.next else None, video_path,
39
- temporary=True)
40
- play_audio_in_external(audio)
41
- gsm_state.previous_audio = audio
42
- elif get_config().advanced.video_player_path:
43
- play_video_in_external(line, video_path)
44
- else:
45
- import sounddevice as sd
46
- import soundfile as sf
47
- audio = get_audio_from_video(line, line.next.time if line.next else None, video_path,
48
- temporary=True)
49
- data, samplerate = sf.read(audio)
50
- sd.play(data, samplerate)
51
- sd.wait()
52
- gsm_state.previous_audio = (data, samplerate)
53
- return
54
- if gsm_state.line_for_screenshot:
55
- line: GameLine = gsm_state.line_for_screenshot
56
- gsm_state.line_for_screenshot = None
57
- gsm_state.previous_line_for_screenshot = line
58
- screenshot = ffmpeg.get_screenshot_for_line(video_path, line, True)
59
- if gsm_state.anki_note_for_screenshot:
60
- gsm_state.anki_note_for_screenshot = None
61
- encoded_image = ffmpeg.process_image(screenshot)
62
- if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
63
- last_note = anki.get_last_anki_card()
64
- if last_note:
65
- anki.add_image_to_card(last_note, encoded_image)
66
- notification.send_screenshot_updated(last_note.get_field(get_config().anki.word_field))
67
- if get_config().features.open_anki_edit:
68
- notification.open_anki_card(last_note.noteId)
69
- else:
70
- notification.send_screenshot_saved(encoded_image)
71
- else:
72
- notification.send_screenshot_saved(encoded_image)
73
- else:
74
- os.startfile(screenshot)
75
- return
76
- except Exception as e:
77
- logger.error(f"Error Playing Audio/Video: {e}")
78
- logger.debug(f"Error Playing Audio/Video: {e}", exc_info=True)
79
- return
80
- finally:
81
- gsm_state.previous_replay = video_path
82
- gsm_state.videos_to_remove.add(video_path)
83
-
84
-
85
- def play_audio_in_external(filepath):
86
- exe = get_config().advanced.audio_player_path
87
-
88
- filepath = os.path.normpath(filepath)
89
-
90
- command = [exe, "--no-video", filepath]
91
-
92
- try:
93
- subprocess.Popen(command)
94
- print(f"Opened {filepath} in {exe}.")
95
- except Exception as e:
96
- print(f"An error occurred: {e}")
97
-
98
-
99
- def play_video_in_external(line, filepath):
100
- command = [get_config().advanced.video_player_path]
101
-
102
- start, _, _, _ = get_video_timings(filepath, line)
103
-
104
- if start:
105
- if "vlc" in get_config().advanced.video_player_path:
106
- command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
107
- else:
108
- command.extend(["--start", convert_to_vlc_seconds(start)])
109
- command.append(os.path.normpath(filepath))
110
-
111
- logger.info(" ".join(command))
112
-
113
-
114
-
115
- try:
116
- subprocess.Popen(command)
117
- logger.info(f"Opened {filepath} in {get_config().advanced.video_player_path}.")
118
- except FileNotFoundError:
119
- logger.error("VLC not found. Make sure it's installed and in your PATH.")
120
- except Exception as e:
121
- logger.error(f"An error occurred: {e}")
122
-
123
-
124
- def convert_to_vlc_seconds(time_str):
125
- """Converts HH:MM:SS.milliseconds to VLC-compatible seconds."""
126
- try:
127
- hours, minutes, seconds_ms = time_str.split(":")
128
- seconds, milliseconds = seconds_ms.split(".")
129
- total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + int(seconds) + (int(milliseconds) / 1000.0)
130
- return str(total_seconds)
131
- except ValueError:
132
- return "Invalid time format"
File without changes