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.
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/anki.py +7 -8
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/config_gui.py +19 -3
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/configuration.py +8 -1
- gamesentenceminer-2.8.1/GameSentenceMiner/downloader/oneocr_dl.py +243 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ffmpeg.py +1 -3
- gamesentenceminer-2.8.1/GameSentenceMiner/gametext.py +131 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/gsm.py +28 -29
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/obs.py +0 -3
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/ocrconfig.py +0 -1
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -1
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/owocr_helper.py +25 -26
- gamesentenceminer-2.8.1/GameSentenceMiner/text_log.py +186 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/util.py +60 -3
- gamesentenceminer-2.8.1/GameSentenceMiner/web/__init__.py +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/__init__.py +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon.ico +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/favicon.svg +3 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/site.webmanifest +21 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/style.css +292 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/text_replacements.html +238 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/utility.html +313 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- gamesentenceminer-2.8.1/GameSentenceMiner/web/texthooking_page.py +234 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/PKG-INFO +2 -1
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/SOURCES.txt +16 -2
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/requires.txt +1 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/PKG-INFO +2 -1
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/pyproject.toml +6 -2
- gamesentenceminer-2.7.17/GameSentenceMiner/gametext.py +0 -270
- gamesentenceminer-2.7.17/GameSentenceMiner/utility_gui.py +0 -204
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ai/gemini.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/send.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/communication/websocket.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/electron_config.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/model.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/notification.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/run.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/package.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/__init__.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/silero_trim.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/vosk_helper.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner/vad/whisper_helper.py +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/LICENSE +0 -0
- {gamesentenceminer-2.7.17 → gamesentenceminer-2.8.1}/README.md +0 -0
- {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
|
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.
|
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 =
|
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=
|
283
|
-
|
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
|
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
|
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.
|
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
|
61
|
-
line: GameLine =
|
62
|
-
|
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
|
71
|
-
line: GameLine =
|
72
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
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(
|
331
|
-
|
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
|
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
|
-
|
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()
|