GameSentenceMiner 2.7.17__tar.gz → 2.8.1__tar.gz

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 (65) hide show
  1. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/anki.py +7 -8
  2. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/config_gui.py +19 -3
  3. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/configuration.py +8 -1
  4. gamesentenceminer-2.8.1/GameSentenceMiner/downloader/oneocr_dl.py +243 -0
  5. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ffmpeg.py +1 -3
  6. gamesentenceminer-2.8.1/GameSentenceMiner/gametext.py +131 -0
  7. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/gsm.py +28 -29
  8. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/obs.py +0 -3
  9. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/ocrconfig.py +0 -1
  10. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -1
  11. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/owocr_helper.py +25 -26
  12. gamesentenceminer-2.8.1/GameSentenceMiner/text_log.py +186 -0
  13. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/util.py +60 -3
  14. gamesentenceminer-2.8.1/GameSentenceMiner/web/__init__.py +0 -0
  15. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/__init__.py +0 -0
  16. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  17. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  18. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon.ico +0 -0
  19. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon.svg +3 -0
  20. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/site.webmanifest +21 -0
  21. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/style.css +292 -0
  22. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/text_replacements.html +238 -0
  23. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/utility.html +313 -0
  24. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  25. gamesentenceminer-2.8.1/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  26. gamesentenceminer-2.8.1/GameSentenceMiner/web/texthooking_page.py +234 -0
  27. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/PKG-INFO +2 -1
  28. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/SOURCES.txt +16 -2
  29. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/requires.txt +1 -0
  30. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/PKG-INFO +2 -1
  31. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/pyproject.toml +6 -2
  32. gamesentenceminer-2.7.17/GameSentenceMiner/gametext.py +0 -270
  33. gamesentenceminer-2.7.17/GameSentenceMiner/utility_gui.py +0 -204
  34. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/__init__.py +0 -0
  35. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ai/__init__.py +0 -0
  36. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ai/gemini.py +0 -0
  37. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/__init__.py +0 -0
  38. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/send.py +0 -0
  39. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/websocket.py +0 -0
  40. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
  41. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/__init__.py +0 -0
  42. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/download_tools.py +0 -0
  43. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/electron_config.py +0 -0
  44. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/model.py +0 -0
  45. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/notification.py +0 -0
  46. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/__init__.py +0 -0
  47. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  48. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  49. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  50. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  51. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  52. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  53. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  54. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  55. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/package.py +0 -0
  56. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/__init__.py +0 -0
  57. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/silero_trim.py +0 -0
  58. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/vosk_helper.py +0 -0
  59. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/whisper_helper.py +0 -0
  60. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  61. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  62. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  63. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/LICENSE +0 -0
  64. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/README.md +0 -0
  65. {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/setup.cfg +0 -0
@@ -7,16 +7,15 @@ import urllib.request
7
7
  from datetime import datetime, timedelta
8
8
  from requests import post
9
9
 
10
- from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
10
+ from GameSentenceMiner import obs, util, notification, ffmpeg
11
11
  from GameSentenceMiner.ai.gemini import translate_with_context
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.configuration import get_config
14
- from GameSentenceMiner.gametext import get_text_event, get_all_lines
15
14
  from GameSentenceMiner.model import AnkiCard
16
- from GameSentenceMiner.utility_gui import get_utility_window
15
+ from GameSentenceMiner.text_log import get_all_lines, get_text_event, get_mined_line
17
16
  from GameSentenceMiner.obs import get_current_game
18
17
  from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue
19
-
18
+ from GameSentenceMiner.web import texthooking_page
20
19
 
21
20
  audio_in_anki = None
22
21
  screenshot_in_anki = None
@@ -171,7 +170,7 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
171
170
  last_note.get_field(get_config().anki.previous_sentence_field):
172
171
  logger.debug(
173
172
  f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and game_line.prev.text and not last_note.get_field(get_config().anki.previous_sentence_field)}")
174
- if selected_lines:
173
+ if selected_lines and selected_lines[0].prev:
175
174
  note['fields'][get_config().anki.previous_sentence_field] = selected_lines[0].prev.text
176
175
  else:
177
176
  note['fields'][get_config().anki.previous_sentence_field] = game_line.prev.text
@@ -277,10 +276,10 @@ def update_new_card():
277
276
  if get_config().obs.get_game_from_scene:
278
277
  obs.update_current_game()
279
278
  if use_prev_audio:
280
- lines = get_utility_window().get_selected_lines()
279
+ lines = texthooking_page.get_selected_lines()
281
280
  with util.lock:
282
- update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=gametext.get_mined_line(last_card, lines), reuse_audio=True)
283
- get_utility_window().reset_checkboxes()
281
+ update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=get_mined_line(last_card, lines), reuse_audio=True)
282
+ texthooking_page.reset_checked_lines()
284
283
  else:
285
284
  logger.info("New card(s) detected! Added to Processing Queue!")
286
285
  card_queue.append(last_card)
@@ -110,7 +110,8 @@ class ConfigApp:
110
110
  websocket_uri=self.websocket_uri.get(),
111
111
  open_config_on_startup=self.open_config_on_startup.get(),
112
112
  open_multimine_on_startup=self.open_multimine_on_startup.get(),
113
- texthook_replacement_regex=self.texthook_replacement_regex.get()
113
+ texthook_replacement_regex=self.texthook_replacement_regex.get(),
114
+ use_both_clipboard_and_websocket=self.use_both_clipboard_and_websocket.get()
114
115
  ),
115
116
  paths=Paths(
116
117
  folder_to_watch=self.folder_to_watch.get(),
@@ -308,6 +309,13 @@ class ConfigApp:
308
309
  self.add_label_and_increment_row(general_frame, "Enable to allow GSM to see clipboard for text and line timing.",
309
310
  row=self.current_row, column=2)
310
311
 
312
+ ttk.Label(general_frame, text="Allow Both:").grid(row=self.current_row, column=0, sticky='W')
313
+ self.use_both_clipboard_and_websocket = tk.BooleanVar(value=self.settings.general.use_both_clipboard_and_websocket)
314
+ ttk.Checkbutton(general_frame, variable=self.use_both_clipboard_and_websocket).grid(row=self.current_row, column=1,
315
+ sticky='W')
316
+ self.add_label_and_increment_row(general_frame, "Enable to allow GSM to accept both clipboard and websocket input at the same time.",
317
+ row=self.current_row, column=2)
318
+
311
319
  ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
312
320
  self.websocket_uri = ttk.Entry(general_frame)
313
321
  self.websocket_uri.insert(0, self.settings.general.websocket_uri)
@@ -329,13 +337,21 @@ class ConfigApp:
329
337
  self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
330
338
  row=self.current_row, column=2)
331
339
 
332
- ttk.Label(general_frame, text="Open Multimine on Startup:").grid(row=self.current_row, column=0, sticky='W')
340
+ ttk.Label(general_frame, text="Open GSM Texthooker on Startup:").grid(row=self.current_row, column=0, sticky='W')
333
341
  self.open_multimine_on_startup = tk.BooleanVar(value=self.settings.general.open_multimine_on_startup)
334
342
  ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup).grid(row=self.current_row, column=1,
335
343
  sticky='W')
336
- self.add_label_and_increment_row(general_frame, "Whether to open multimining window when the script starts.",
344
+ self.add_label_and_increment_row(general_frame, "Whether to open Texthooking page when the script starts.",
337
345
  row=self.current_row, column=2)
338
346
 
347
+ ttk.Label(general_frame, text="GSM Texthooker Port:").grid(row=self.current_row, column=0, sticky='W')
348
+ self.texthooker_port = ttk.Entry(general_frame)
349
+ self.texthooker_port.insert(0, str(self.settings.general.texthooker_port))
350
+ self.texthooker_port.grid(row=self.current_row, column=1)
351
+ self.add_label_and_increment_row(general_frame, "Port for the Texthooker to run on. Only change if you know what you are doing", row=self.current_row,
352
+ column=2)
353
+
354
+
339
355
  ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
340
356
  self.current_version = ttk.Label(general_frame, text=get_current_version())
341
357
  self.current_version.grid(row=self.current_row, column=1)
@@ -2,6 +2,7 @@ import json
2
2
  import logging
3
3
  import os
4
4
  import shutil
5
+ import socket
5
6
  from dataclasses import dataclass, field
6
7
  from logging.handlers import RotatingFileHandler
7
8
  from os.path import expanduser
@@ -36,16 +37,17 @@ DEFAULT_CONFIG = 'Default'
36
37
 
37
38
  current_game = ''
38
39
 
39
-
40
40
  @dataclass_json
41
41
  @dataclass
42
42
  class General:
43
43
  use_websocket: bool = True
44
44
  use_clipboard: bool = True
45
+ use_both_clipboard_and_websocket: bool = False
45
46
  websocket_uri: str = 'localhost:6677'
46
47
  open_config_on_startup: bool = False
47
48
  open_multimine_on_startup: bool = False
48
49
  texthook_replacement_regex: str = ""
50
+ texthooker_port: int = 55000
49
51
 
50
52
 
51
53
  @dataclass_json
@@ -344,11 +346,16 @@ class Config:
344
346
  self.sync_shared_field(config.general, profile.general, "open_config_on_startup")
345
347
  self.sync_shared_field(config.general, profile.general, "open_multimine_on_startup")
346
348
  self.sync_shared_field(config.general, profile.general, "websocket_uri")
349
+ self.sync_shared_field(config.general, profile.general, "texthooker_port")
347
350
  self.sync_shared_field(config.audio, profile.audio, "external_tool")
348
351
  self.sync_shared_field(config.audio, profile.audio, "anki_media_collection")
349
352
  self.sync_shared_field(config, profile, "advanced")
350
353
  self.sync_shared_field(config, profile, "paths")
351
354
  self.sync_shared_field(config, profile, "obs")
355
+ self.sync_shared_field(config.ai, profile.ai, "anki_field")
356
+ self.sync_shared_field(config.ai, profile.ai, "provider")
357
+ self.sync_shared_field(config.ai, profile.ai, "api_key")
358
+
352
359
 
353
360
  return self
354
361
 
@@ -0,0 +1,243 @@
1
+ import os
2
+ import zipfile
3
+ import shutil
4
+ from os.path import expanduser
5
+
6
+ import requests
7
+ import re
8
+ import tempfile
9
+
10
+ # Placeholder functions/constants for removed proprietary ones
11
+ # In a real application, you would replace these with appropriate logic
12
+ # or standard library equivalents.
13
+
14
+ def checkdir(d):
15
+ """Checks if a directory exists and contains the expected files."""
16
+ flist = ["oneocr.dll", "oneocr.onemodel", "onnxruntime.dll"]
17
+ return os.path.isdir(d) and all((os.path.isfile(os.path.join(d, _)) for _ in flist))
18
+
19
+ def selectdir():
20
+ """Attempts to find the SnippingTool directory, prioritizing cache."""
21
+ cachedir = "cache/SnippingTool"
22
+ packageFamilyName = "Microsoft.ScreenSketch_8wekyb3d8bbwe"
23
+
24
+ if checkdir(cachedir):
25
+ return cachedir
26
+ # This part needs NativeUtils.GetPackagePathByPackageFamily, which is proprietary.
27
+ # We'll skip this part for simplification as requested.
28
+ # path = NativeUtils.GetPackagePathByPackageFamily(packageFamilyName)
29
+ # if not path:
30
+ # return None
31
+ # path = os.path.join(path, "SnippingTool")
32
+ # if not checkdir(path):
33
+ # return None
34
+ # return path
35
+ return None # Return None if not found in cache
36
+
37
+ def getproxy():
38
+ """Placeholder for proxy retrieval."""
39
+ # Replace with actual proxy retrieval logic or return None
40
+ return None
41
+
42
+ def stringfyerror(e):
43
+ """Placeholder for error stringification."""
44
+ return str(e)
45
+
46
+ def dynamiclink(path):
47
+ """Placeholder for dynamic link resolution."""
48
+ # This would likely map a resource path to a local file path.
49
+ # For simplification, we'll just use the provided path string.
50
+ return path # Assuming path is a URL here based on usage
51
+
52
+ # Simplified download logic extracted from the question class
53
+ class Downloader:
54
+ def __init__(self):
55
+ self.oneocr_dir = expanduser("~/.config/oneocr")
56
+ self.packageFamilyName = "Microsoft.ScreenSketch_8wekyb3d8bbwe"
57
+ self.flist = ["oneocr.dll", "oneocr.onemodel", "onnxruntime.dll"]
58
+
59
+ def download_and_extract(self):
60
+ """
61
+ Main function to attempt download and extraction.
62
+ Tries official source first, then a fallback URL.
63
+ """
64
+ if checkdir(self.oneocr_dir):
65
+ print("Files already exist in cache.")
66
+ return True
67
+
68
+ try:
69
+ print("Attempting to download from official source...")
70
+ self.downloadofficial()
71
+ print("Download and extraction from official source successful.")
72
+ return True
73
+ except Exception as e:
74
+ print(f"Download from official source failed: {stringfyerror(e)}")
75
+ print("Attempting to download from fallback URL...")
76
+ try:
77
+ fallback_url = dynamiclink("/Resource/SnippingTool") # Assuming this resolves to a URL
78
+ self.downloadx(fallback_url)
79
+ print("Download and extraction from fallback URL successful.")
80
+ return True
81
+ except Exception as e_fallback:
82
+ print(f"Download from fallback URL failed: {stringfyerror(e_fallback)}")
83
+ print("All download attempts failed.")
84
+ return False
85
+
86
+
87
+ def downloadofficial(self):
88
+ """Downloads the latest SnippingTool MSIX bundle from a store API."""
89
+ headers = {
90
+ "accept": "*/*",
91
+ # Changed accept-language to prioritize US English
92
+ "accept-language": "en-US,en;q=0.9",
93
+ "cache-control": "no-cache",
94
+ "origin": "https://store.rg-adguard.net",
95
+ "pragma": "no-cache",
96
+ "priority": "u=1, i",
97
+ "referer": "https://store.rg-adguard.net/",
98
+ "sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
99
+ "sec-ch-ua-mobile": "?0",
100
+ "sec-ch-ua-platform": '"Windows"',
101
+ "sec-fetch-dest": "empty",
102
+ "sec-fetch-mode": "cors",
103
+ "sec-fetch-site": "same-origin",
104
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
105
+ }
106
+
107
+ data = dict(type="PackageFamilyName", url=self.packageFamilyName)
108
+
109
+ response = requests.post(
110
+ "https://store.rg-adguard.net/api/GetFiles",
111
+ headers=headers,
112
+ data=data,
113
+ proxies=getproxy(),
114
+ )
115
+ response.raise_for_status() # Raise an exception for bad status codes
116
+
117
+ saves = []
118
+ for link, package in re.findall('<a href="(.*?)".*?>(.*?)</a>', response.text):
119
+ if not package.startswith("Microsoft.ScreenSketch"):
120
+ continue
121
+ if not package.endswith(".msixbundle"):
122
+ continue
123
+ version = re.search(r"\d+\.\d+\.\d+\.\d+", package)
124
+ if not version:
125
+ continue
126
+ version = tuple(int(_) for _ in version.group().split("."))
127
+ saves.append((version, link, package))
128
+
129
+ if not saves:
130
+ raise Exception("Could not find suitable download link from official source.")
131
+
132
+ saves.sort(key=lambda _: _[0])
133
+ url = saves[-1][1]
134
+ package_name = saves[-1][2]
135
+
136
+ print(f"Downloading {package_name} from {url}")
137
+ req = requests.get(url, stream=True, proxies=getproxy())
138
+ req.raise_for_status()
139
+
140
+ total_size_in_bytes = int(req.headers.get('content-length', 0))
141
+ block_size = 1024 * 32 # 32 Kibibytes
142
+ temp_msixbundle_path = os.path.join(tempfile.gettempdir(), package_name)
143
+
144
+ with open(temp_msixbundle_path, "wb") as ff:
145
+ downloaded_size = 0
146
+ for chunk in req.iter_content(chunk_size=block_size):
147
+ ff.write(chunk)
148
+ downloaded_size += len(chunk)
149
+ # Basic progress reporting (can be removed)
150
+ if total_size_in_bytes:
151
+ progress = (downloaded_size / total_size_in_bytes) * 100
152
+ print(f"Downloaded {downloaded_size}/{total_size_in_bytes} bytes ({progress:.2f}%)", end='\r')
153
+ print("\nDownload complete. Extracting...")
154
+
155
+ namemsix = None
156
+ with zipfile.ZipFile(temp_msixbundle_path) as ff:
157
+ for name in ff.namelist():
158
+ if name.startswith("SnippingTool") and name.endswith("_x64.msix"):
159
+ namemsix = name
160
+ break
161
+ if not namemsix:
162
+ raise Exception("Could not find MSIX file within MSIXBUNDLE.")
163
+ temp_msix_path = os.path.join(tempfile.gettempdir(), namemsix)
164
+ ff.extract(namemsix, tempfile.gettempdir())
165
+
166
+ print(f"Extracted {namemsix}. Extracting components...")
167
+ if os.path.exists(self.oneocr_dir):
168
+ shutil.rmtree(self.oneocr_dir)
169
+ os.makedirs(self.oneocr_dir, exist_ok=True)
170
+
171
+ with zipfile.ZipFile(temp_msix_path) as ff:
172
+ collect = []
173
+ for name in ff.namelist():
174
+ # Extract only the files within the "SnippingTool/" directory
175
+ if name.startswith("SnippingTool/") and any(name.endswith(f) for f in self.flist):
176
+ # Construct target path relative to cachedir
177
+ target_path = os.path.join(self.oneocr_dir, os.path.relpath(name, "SnippingTool/"))
178
+ # Ensure parent directories exist
179
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
180
+ # Extract the file
181
+ with ff.open(name) as source, open(target_path, "wb") as target:
182
+ shutil.copyfileobj(source, target)
183
+ collect.append(name)
184
+ if not collect:
185
+ raise Exception("Could not find required files within MSIX.")
186
+
187
+
188
+ if not checkdir(self.oneocr_dir):
189
+ raise Exception("Extraction failed: Required files not found in cache directory.")
190
+
191
+ # Clean up temporary files
192
+ os.remove(temp_msixbundle_path)
193
+ os.remove(temp_msix_path)
194
+
195
+
196
+ def downloadx(self, url: str):
197
+ """Downloads a zip file from a URL and extracts it."""
198
+ print(f"Downloading from fallback URL: {url}")
199
+ # Added accept-language to the fallback download as well for consistency
200
+ headers = {
201
+ "accept-language": "en-US,en;q=0.9",
202
+ # Add other relevant headers if necessary for the fallback URL
203
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
204
+ "accept": "*/*",
205
+ }
206
+ req = requests.get(url, verify=False, proxies=getproxy(), stream=True, headers=headers)
207
+ req.raise_for_status()
208
+
209
+ total_size_in_bytes = int(req.headers.get('content-length', 0))
210
+ block_size = 1024 * 32 # 32 Kibibytes
211
+ temp_zip_path = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
212
+
213
+ with open(temp_zip_path, "wb") as ff:
214
+ downloaded_size = 0
215
+ for chunk in req.iter_content(chunk_size=block_size):
216
+ ff.write(chunk)
217
+ downloaded_size += len(chunk)
218
+ # Basic progress reporting (can be removed)
219
+ if total_size_in_bytes:
220
+ progress = (downloaded_size / total_size_in_bytes) * 100
221
+ print(f"Downloaded {downloaded_size}/{total_size_in_bytes} bytes ({progress:.2f}%)", end='\r')
222
+ print("\nDownload complete. Extracting...")
223
+
224
+ if os.path.exists(self.oneocr_dir):
225
+ shutil.rmtree(self.oneocr_dir)
226
+ os.makedirs(self.oneocr_dir, exist_ok=True)
227
+
228
+ with zipfile.ZipFile(temp_zip_path) as zipf:
229
+ zipf.extractall(self.oneocr_dir)
230
+
231
+ if not checkdir(self.oneocr_dir):
232
+ raise Exception("Extraction failed: Required files not found in cache directory.")
233
+
234
+ # Clean up temporary files
235
+ os.remove(temp_zip_path)
236
+
237
+ # Example usage:
238
+ if __name__ == "__main__":
239
+ downloader = Downloader()
240
+ if downloader.download_and_extract():
241
+ print("SnippingTool files are ready.")
242
+ else:
243
+ print("Failed to obtain SnippingTool files.")
@@ -1,11 +1,9 @@
1
- import subprocess
2
1
  import tempfile
3
- import time
4
2
 
5
3
  from GameSentenceMiner import obs, util, configuration
6
4
  from GameSentenceMiner.configuration import *
5
+ from GameSentenceMiner.text_log import initial_time
7
6
  from GameSentenceMiner.util import *
8
- from GameSentenceMiner.gametext import initial_time
9
7
 
10
8
 
11
9
  def get_ffmpeg_path():
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ import re
3
+ import threading
4
+ import time
5
+
6
+ import pyperclip
7
+ import websockets
8
+ from websockets import InvalidStatus
9
+
10
+ from GameSentenceMiner import util
11
+ from GameSentenceMiner.configuration import *
12
+ from GameSentenceMiner.text_log import *
13
+ from GameSentenceMiner.util import do_text_replacements, TEXT_REPLACEMENTS_FILE
14
+
15
+ from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker
16
+
17
+ current_line = ''
18
+ current_line_after_regex = ''
19
+ current_line_time = datetime.now()
20
+
21
+ reconnecting = False
22
+ websocket_connected = False
23
+
24
+ # def remove_old_events(self, cutoff_time: datetime):
25
+ # self.values = [line for line in self.values if line.time >= cutoff_time]
26
+
27
+ class ClipboardMonitor(threading.Thread):
28
+
29
+ def __init__(self):
30
+ threading.Thread.__init__(self)
31
+ self.daemon = True
32
+
33
+ def run(self):
34
+ global current_line_time, current_line
35
+
36
+ # Initial clipboard content
37
+ current_line = pyperclip.paste()
38
+
39
+ skip_next_clipboard = False
40
+ while True:
41
+ if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
42
+ time.sleep(1)
43
+ skip_next_clipboard = True
44
+ continue
45
+ current_clipboard = pyperclip.paste()
46
+
47
+ if current_clipboard != current_line and not skip_next_clipboard:
48
+ handle_new_text_event(current_clipboard)
49
+ skip_next_clipboard = False
50
+
51
+ time.sleep(0.05)
52
+
53
+
54
+ async def listen_websocket():
55
+ global current_line, current_line_time, reconnecting, websocket_connected
56
+ try_other = False
57
+ websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
58
+ while True:
59
+ if try_other:
60
+ websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
61
+ try:
62
+ async with websockets.connect(websocket_url, ping_interval=None) as websocket:
63
+ logger.info("TextHooker Websocket Connected!")
64
+ if reconnecting:
65
+ logger.info(f"Texthooker WebSocket connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
66
+ reconnecting = False
67
+ websocket_connected = True
68
+ try_other = True
69
+ line_time = None
70
+ while True:
71
+ message = await websocket.recv()
72
+ logger.debug(message)
73
+ try:
74
+ data = json.loads(message)
75
+ if "sentence" in data:
76
+ current_clipboard = data["sentence"]
77
+ if "time" in data:
78
+ line_time = datetime.fromisoformat(data["time"])
79
+ print(line_time)
80
+ except json.JSONDecodeError or TypeError:
81
+ current_clipboard = message
82
+ if current_clipboard != current_line:
83
+ handle_new_text_event(current_clipboard, line_time if line_time else None)
84
+ except (websockets.ConnectionClosed, ConnectionError, InvalidStatus) as e:
85
+ if isinstance(e, InvalidStatus):
86
+ e: InvalidStatus
87
+ if e.response.status_code == 404:
88
+ logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
89
+ try_other = True
90
+
91
+ logger.error(f"Texthooker WebSocket connection failed. Please check if the Texthooker is running and the WebSocket URI is correct.")
92
+ websocket_connected = False
93
+ if not reconnecting:
94
+ logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
95
+ reconnecting = True
96
+ await asyncio.sleep(5)
97
+
98
+ def handle_new_text_event(current_clipboard, line_time=None):
99
+ global current_line, current_line_time, current_line_after_regex
100
+ current_line = current_clipboard
101
+ if get_config().general.texthook_replacement_regex:
102
+ current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
103
+ else:
104
+ current_line_after_regex = current_line
105
+ current_line_after_regex = do_text_replacements(current_line, TEXT_REPLACEMENTS_FILE)
106
+ logger.info(f"Line Received: {current_line_after_regex}")
107
+ current_line_time = line_time if line_time else datetime.now()
108
+ add_line(current_line_after_regex, line_time)
109
+ add_event_to_texthooker(get_text_log()[-1])
110
+
111
+ def reset_line_hotkey_pressed():
112
+ global current_line_time
113
+ logger.info("LINE RESET HOTKEY PRESSED")
114
+ current_line_time = datetime.now()
115
+ util.set_last_mined_line("")
116
+
117
+
118
+ def run_websocket_listener():
119
+ asyncio.run(listen_websocket())
120
+
121
+
122
+ def start_text_monitor():
123
+ if get_config().general.use_websocket:
124
+ threading.Thread(target=run_websocket_listener, daemon=True).start()
125
+ if get_config().general.use_clipboard:
126
+ if get_config().general.use_websocket:
127
+ if get_config().general.use_both_clipboard_and_websocket:
128
+ logger.info("Listening for Text on both WebSocket and Clipboard.")
129
+ else:
130
+ logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
131
+ ClipboardMonitor().start()
@@ -10,7 +10,6 @@ from pystray import Icon, Menu, MenuItem
10
10
  from watchdog.events import FileSystemEventHandler
11
11
  from watchdog.observers import Observer
12
12
 
13
- import time
14
13
 
15
14
  from GameSentenceMiner import anki
16
15
  from GameSentenceMiner import config_gui
@@ -27,10 +26,11 @@ from GameSentenceMiner.communication.websocket import connect_websocket, registe
27
26
  from GameSentenceMiner.configuration import *
28
27
  from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
29
28
  from GameSentenceMiner.ffmpeg import get_audio_and_trim, get_video_timings
30
- from GameSentenceMiner.gametext import get_text_event, get_mined_line, GameLine
31
29
  from GameSentenceMiner.obs import check_obs_folder_is_correct
30
+ from GameSentenceMiner.text_log import GameLine, get_text_event, get_mined_line, get_all_lines
32
31
  from GameSentenceMiner.util import *
33
- from GameSentenceMiner.utility_gui import init_utility_window, get_utility_window
32
+ from GameSentenceMiner.web import texthooking_page
33
+ from GameSentenceMiner.web.texthooking_page import start_web_server
34
34
 
35
35
  if is_windows():
36
36
  import win32api
@@ -57,9 +57,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
57
57
  @staticmethod
58
58
  def convert_to_audio(video_path):
59
59
  try:
60
- if get_utility_window().line_for_audio:
61
- line: GameLine = get_utility_window().line_for_audio
62
- get_utility_window().line_for_audio = None
60
+ if texthooking_page.event_manager.line_for_audio:
61
+ line: GameLine = texthooking_page.event_manager.line_for_audio
62
+ texthooking_page.event_manager.line_for_audio = None
63
63
  if get_config().advanced.audio_player_path:
64
64
  audio = VideoToAudioHandler.get_audio(line, line.next.time if line.next else None, video_path, temporary=True)
65
65
  play_audio_in_external(audio)
@@ -67,9 +67,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
67
67
  elif get_config().advanced.video_player_path:
68
68
  play_video_in_external(line, video_path)
69
69
  return
70
- if get_utility_window().line_for_screenshot:
71
- line: GameLine = get_utility_window().line_for_screenshot
72
- get_utility_window().line_for_screenshot = None
70
+ if texthooking_page.event_manager.line_for_screenshot:
71
+ line: GameLine = texthooking_page.event_manager.line_for_screenshot
72
+ texthooking_page.event_manager.line_for_screenshot = None
73
73
  screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
74
74
  os.startfile(screenshot)
75
75
  os.remove(video_path)
@@ -109,18 +109,18 @@ class VideoToAudioHandler(FileSystemEventHandler):
109
109
  if mined_line.next:
110
110
  line_cutoff = mined_line.next.time
111
111
 
112
- if get_utility_window().lines_selected():
113
- lines = get_utility_window().get_selected_lines()
114
- start_line = lines[0]
115
- mined_line = get_mined_line(last_note, lines)
116
- line_cutoff = get_utility_window().get_next_line_timing()
112
+ selected_lines = []
113
+ if texthooking_page.are_lines_selected():
114
+ selected_lines = texthooking_page.get_selected_lines()
115
+ start_line = selected_lines[0]
116
+ mined_line = get_mined_line(last_note, selected_lines)
117
+ line_cutoff = selected_lines[-1].get_next_time()
117
118
 
118
119
  if last_note:
119
120
  logger.debug(last_note.to_json())
120
- selected_lines = get_utility_window().get_selected_lines()
121
121
  note = anki.get_initial_card_info(last_note, selected_lines)
122
122
  tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
123
- get_utility_window().reset_checkboxes()
123
+ texthooking_page.reset_checked_lines()
124
124
 
125
125
  if get_config().anki.sentence_audio_field and get_config().audio.enabled:
126
126
  logger.debug("Attempting to get audio from video")
@@ -268,10 +268,10 @@ def register_hotkeys():
268
268
  keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
269
269
  if get_config().hotkeys.take_screenshot:
270
270
  keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
271
- if get_config().hotkeys.open_utility:
272
- keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
273
271
  if get_config().hotkeys.play_latest_audio:
274
272
  keyboard.add_hotkey(get_config().hotkeys.play_latest_audio, play_most_recent_audio)
273
+ if get_config().hotkeys.open_utility:
274
+ keyboard.add_hotkey(get_config().hotkeys.open_utility, texthooking_page.open_texthooker)
275
275
 
276
276
 
277
277
  def get_screenshot():
@@ -322,13 +322,10 @@ def open_settings():
322
322
  settings_window.show()
323
323
 
324
324
 
325
- def open_multimine():
326
- obs.update_current_game()
327
- get_utility_window().show()
328
-
329
325
  def play_most_recent_audio():
330
- if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(gametext.line_history.values) > 0:
331
- get_utility_window().line_for_audio = gametext.line_history.values[-1]
326
+ if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(
327
+ get_all_lines()) > 0:
328
+ texthooking_page.event_manager.line_for_audio = get_all_lines()[-1]
332
329
  obs.save_replay_buffer()
333
330
  else:
334
331
  logger.error("Feature Disabled. No audio or video player path set in config!")
@@ -368,6 +365,10 @@ def play_pause(icon, item):
368
365
  update_icon()
369
366
 
370
367
 
368
+ def open_multimine(icon, item):
369
+ texthooking_page.open_texthooker()
370
+
371
+
371
372
  def update_icon():
372
373
  global menu, icon
373
374
  # Recreate the menu with the updated button text
@@ -416,7 +417,7 @@ def run_tray():
416
417
 
417
418
  menu = Menu(
418
419
  MenuItem("Open Settings", open_settings),
419
- MenuItem("Open Multi-Mine GUI", open_multimine),
420
+ MenuItem("Open Texthooker", texthooking_page.open_texthooker),
420
421
  MenuItem("Open Log", open_log),
421
422
  MenuItem("Toggle Replay Buffer", play_pause),
422
423
  MenuItem("Restart OBS", restart_obs),
@@ -554,6 +555,7 @@ def post_init():
554
555
  whisper_helper.initialize_whisper_model()
555
556
  if get_config().vad.is_silero():
556
557
  from GameSentenceMiner.vad import silero_trim
558
+ start_web_server()
557
559
 
558
560
  util.run_new_thread(do_post_init)
559
561
 
@@ -576,7 +578,6 @@ def main(reloading=False):
576
578
  logger.info("Script started.")
577
579
  root = ttk.Window(themename='darkly')
578
580
  settings_window = config_gui.ConfigApp(root)
579
- init_utility_window(root)
580
581
  initialize(reloading)
581
582
  initialize_async()
582
583
  observer = Observer()
@@ -594,9 +595,7 @@ def main(reloading=False):
594
595
  try:
595
596
  # if get_config().general.open_config_on_startup:
596
597
  # root.after(0, settings_window.show)
597
- if get_config().general.open_multimine_on_startup:
598
- root.after(0, get_utility_window().show)
599
- root.after(0, post_init)
598
+ root.after(50, post_init)
600
599
  settings_window.add_save_hook(update_icon)
601
600
  settings_window.on_exit = exit_program
602
601
  root.mainloop()