GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/__init__.py +39 -0
- GameSentenceMiner/anki.py +6 -3
- GameSentenceMiner/gametext.py +13 -2
- GameSentenceMiner/gsm.py +40 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +4 -1
- GameSentenceMiner/owocr/owocr/ocr.py +304 -134
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/ui/anki_confirmation.py +4 -2
- GameSentenceMiner/ui/config_gui.py +12 -0
- GameSentenceMiner/util/configuration.py +6 -2
- GameSentenceMiner/util/cron/__init__.py +12 -0
- GameSentenceMiner/util/cron/daily_rollup.py +613 -0
- GameSentenceMiner/util/cron/jiten_update.py +397 -0
- GameSentenceMiner/util/cron/populate_games.py +154 -0
- GameSentenceMiner/util/cron/run_crons.py +148 -0
- GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
- GameSentenceMiner/util/cron_table.py +334 -0
- GameSentenceMiner/util/db.py +236 -49
- GameSentenceMiner/util/ffmpeg.py +23 -4
- GameSentenceMiner/util/games_table.py +340 -93
- GameSentenceMiner/util/jiten_api_client.py +188 -0
- GameSentenceMiner/util/stats_rollup_table.py +216 -0
- GameSentenceMiner/web/anki_api_endpoints.py +438 -220
- GameSentenceMiner/web/database_api.py +955 -1259
- GameSentenceMiner/web/jiten_database_api.py +1015 -0
- GameSentenceMiner/web/rollup_stats.py +672 -0
- GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
- GameSentenceMiner/web/static/css/overview.css +604 -47
- GameSentenceMiner/web/static/css/search.css +226 -0
- GameSentenceMiner/web/static/css/shared.css +762 -0
- GameSentenceMiner/web/static/css/stats.css +221 -0
- GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
- GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
- GameSentenceMiner/web/static/js/database-game-data.js +390 -0
- GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
- GameSentenceMiner/web/static/js/database-helpers.js +44 -0
- GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
- GameSentenceMiner/web/static/js/database-popups.js +89 -0
- GameSentenceMiner/web/static/js/database-tabs.js +64 -0
- GameSentenceMiner/web/static/js/database-text-management.js +371 -0
- GameSentenceMiner/web/static/js/database.js +86 -718
- GameSentenceMiner/web/static/js/goals.js +79 -18
- GameSentenceMiner/web/static/js/heatmap.js +29 -23
- GameSentenceMiner/web/static/js/overview.js +1205 -339
- GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
- GameSentenceMiner/web/static/js/search.js +215 -18
- GameSentenceMiner/web/static/js/shared.js +193 -39
- GameSentenceMiner/web/static/js/stats.js +1536 -179
- GameSentenceMiner/web/stats.py +1142 -269
- GameSentenceMiner/web/stats_api.py +2104 -0
- GameSentenceMiner/web/templates/anki_stats.html +4 -18
- GameSentenceMiner/web/templates/components/date-range.html +118 -3
- GameSentenceMiner/web/templates/components/html-head.html +40 -6
- GameSentenceMiner/web/templates/components/js-config.html +8 -8
- GameSentenceMiner/web/templates/components/regex-input.html +160 -0
- GameSentenceMiner/web/templates/database.html +564 -117
- GameSentenceMiner/web/templates/goals.html +41 -5
- GameSentenceMiner/web/templates/overview.html +159 -129
- GameSentenceMiner/web/templates/search.html +78 -9
- GameSentenceMiner/web/templates/stats.html +159 -5
- GameSentenceMiner/web/texthooking_page.py +280 -111
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ import threading
|
|
|
6
6
|
|
|
7
7
|
import flask
|
|
8
8
|
import webbrowser
|
|
9
|
+
from flask import make_response
|
|
9
10
|
|
|
10
11
|
from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
|
|
11
12
|
from GameSentenceMiner.obs import get_current_game
|
|
@@ -13,26 +14,47 @@ from GameSentenceMiner.util.gsm_utils import TEXT_REPLACEMENTS_FILE
|
|
|
13
14
|
from GameSentenceMiner.util.text_log import get_line_by_id, get_all_lines
|
|
14
15
|
from flask import render_template, request, jsonify, send_from_directory
|
|
15
16
|
from GameSentenceMiner import obs
|
|
16
|
-
from GameSentenceMiner.util.configuration import
|
|
17
|
+
from GameSentenceMiner.util.configuration import (
|
|
18
|
+
logger,
|
|
19
|
+
get_config,
|
|
20
|
+
gsm_state,
|
|
21
|
+
gsm_status,
|
|
22
|
+
)
|
|
17
23
|
from GameSentenceMiner.web.service import handle_texthooker_button
|
|
18
24
|
|
|
19
25
|
# Import from new modules
|
|
20
|
-
from GameSentenceMiner.web.events import
|
|
21
|
-
EventManager, event_manager
|
|
22
|
-
)
|
|
26
|
+
from GameSentenceMiner.web.events import EventManager, event_manager
|
|
23
27
|
from GameSentenceMiner.web.stats import (
|
|
24
|
-
is_kanji,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
is_kanji,
|
|
29
|
+
interpolate_color,
|
|
30
|
+
get_gradient_color,
|
|
31
|
+
calculate_kanji_frequency,
|
|
32
|
+
calculate_heatmap_data,
|
|
33
|
+
calculate_total_chars_per_game,
|
|
34
|
+
calculate_reading_time_per_game,
|
|
35
|
+
calculate_reading_speed_per_game,
|
|
36
|
+
generate_game_colors,
|
|
37
|
+
format_large_number,
|
|
38
|
+
calculate_actual_reading_time,
|
|
39
|
+
calculate_daily_reading_time,
|
|
40
|
+
calculate_time_based_streak,
|
|
41
|
+
format_time_human_readable,
|
|
42
|
+
calculate_current_game_stats,
|
|
43
|
+
calculate_all_games_stats,
|
|
29
44
|
)
|
|
30
45
|
from GameSentenceMiner.web.gsm_websocket import (
|
|
31
|
-
WebsocketServerThread,
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
WebsocketServerThread,
|
|
47
|
+
websocket_queue,
|
|
48
|
+
paused,
|
|
49
|
+
websocket_server_thread,
|
|
50
|
+
plaintext_websocket_server_thread,
|
|
51
|
+
overlay_server_thread,
|
|
52
|
+
websocket_server_threads,
|
|
53
|
+
handle_exit_signal,
|
|
34
54
|
)
|
|
35
55
|
from GameSentenceMiner.web.database_api import register_database_api_routes
|
|
56
|
+
from GameSentenceMiner.web.jiten_database_api import register_jiten_database_api_routes
|
|
57
|
+
from GameSentenceMiner.web.stats_api import register_stats_api_routes
|
|
36
58
|
|
|
37
59
|
# Global configuration
|
|
38
60
|
port = get_config().general.texthooker_port
|
|
@@ -43,27 +65,102 @@ server_start_time = datetime.datetime.now().timestamp()
|
|
|
43
65
|
|
|
44
66
|
app = flask.Flask(__name__)
|
|
45
67
|
|
|
68
|
+
# Configure Flask-Compress for Brotli compression
|
|
69
|
+
try:
|
|
70
|
+
from flask_compress import Compress
|
|
71
|
+
|
|
72
|
+
# Configure compression settings
|
|
73
|
+
app.config["COMPRESS_MIMETYPES"] = [
|
|
74
|
+
"text/html",
|
|
75
|
+
"text/css",
|
|
76
|
+
"text/xml",
|
|
77
|
+
"text/plain",
|
|
78
|
+
"application/json",
|
|
79
|
+
"application/javascript",
|
|
80
|
+
"application/x-javascript",
|
|
81
|
+
"text/javascript",
|
|
82
|
+
]
|
|
83
|
+
app.config["COMPRESS_LEVEL"] = 6 # Balance between speed and compression ratio
|
|
84
|
+
app.config["COMPRESS_MIN_SIZE"] = 500 # Only compress files larger than 500 bytes
|
|
85
|
+
app.config["COMPRESS_ALGORITHM"] = [
|
|
86
|
+
"br",
|
|
87
|
+
"gzip",
|
|
88
|
+
"deflate",
|
|
89
|
+
] # Prefer Brotli, fallback to gzip
|
|
90
|
+
|
|
91
|
+
Compress(app)
|
|
92
|
+
logger.info("Flask compression enabled with Brotli support")
|
|
93
|
+
except ImportError:
|
|
94
|
+
logger.warning(
|
|
95
|
+
"flask-compress not installed. Run 'pip install flask-compress' for better performance."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Add cache control headers for static files
|
|
100
|
+
@app.after_request
|
|
101
|
+
def add_cache_headers(response):
|
|
102
|
+
"""Add cache control headers to static assets for better performance."""
|
|
103
|
+
# Only add cache headers for static files (CSS, JS, images, fonts)
|
|
104
|
+
if request.path.startswith("/static/"):
|
|
105
|
+
# Check file extension
|
|
106
|
+
if any(request.path.endswith(ext) for ext in [".css", ".js"]):
|
|
107
|
+
# No cache for CSS/JS - always fetch fresh content
|
|
108
|
+
response.cache_control.no_cache = True
|
|
109
|
+
response.cache_control.no_store = True
|
|
110
|
+
response.cache_control.must_revalidate = True
|
|
111
|
+
response.headers["Cache-Control"] = (
|
|
112
|
+
"no-cache, no-store, must-revalidate"
|
|
113
|
+
)
|
|
114
|
+
response.headers["Pragma"] = "no-cache"
|
|
115
|
+
response.headers["Expires"] = "0"
|
|
116
|
+
elif any(
|
|
117
|
+
request.path.endswith(ext)
|
|
118
|
+
for ext in [
|
|
119
|
+
".jpg",
|
|
120
|
+
".jpeg",
|
|
121
|
+
".png",
|
|
122
|
+
".gif",
|
|
123
|
+
".svg",
|
|
124
|
+
".woff",
|
|
125
|
+
".woff2",
|
|
126
|
+
".ttf",
|
|
127
|
+
".eot",
|
|
128
|
+
".ico",
|
|
129
|
+
]
|
|
130
|
+
):
|
|
131
|
+
# Cache images and fonts for longer (they rarely change)
|
|
132
|
+
response.cache_control.max_age = 2592000 # 30 days
|
|
133
|
+
response.cache_control.public = True
|
|
134
|
+
response.headers["Cache-Control"] = "public, max-age=2592000, immutable"
|
|
135
|
+
return response
|
|
136
|
+
|
|
137
|
+
|
|
46
138
|
# Register database API routes
|
|
47
139
|
register_database_api_routes(app)
|
|
140
|
+
register_jiten_database_api_routes(app)
|
|
141
|
+
register_stats_api_routes(app)
|
|
48
142
|
|
|
49
143
|
# Register Anki API routes
|
|
50
144
|
from GameSentenceMiner.web.anki_api_endpoints import register_anki_api_endpoints
|
|
145
|
+
|
|
51
146
|
register_anki_api_endpoints(app)
|
|
52
147
|
|
|
148
|
+
|
|
53
149
|
# Load data from the JSON file
|
|
54
150
|
def load_data_from_file():
|
|
55
151
|
if os.path.exists(TEXT_REPLACEMENTS_FILE):
|
|
56
|
-
with open(TEXT_REPLACEMENTS_FILE,
|
|
152
|
+
with open(TEXT_REPLACEMENTS_FILE, "r", encoding="utf-8") as file:
|
|
57
153
|
return json.load(file)
|
|
58
154
|
return {"enabled": True, "args": {"replacements": {}}}
|
|
59
155
|
|
|
156
|
+
|
|
60
157
|
# Save data to the JSON file
|
|
61
158
|
def save_data_to_file(data):
|
|
62
|
-
with open(TEXT_REPLACEMENTS_FILE,
|
|
159
|
+
with open(TEXT_REPLACEMENTS_FILE, "w", encoding="utf-8") as file:
|
|
63
160
|
json.dump(data, file, indent=4, ensure_ascii=False)
|
|
64
161
|
|
|
65
162
|
|
|
66
|
-
@app.route(
|
|
163
|
+
@app.route("/load-data", methods=["GET"])
|
|
67
164
|
def load_data():
|
|
68
165
|
try:
|
|
69
166
|
data = load_data_from_file()
|
|
@@ -72,7 +169,7 @@ def load_data():
|
|
|
72
169
|
return jsonify({"error": f"Failed to load data: {str(e)}"}), 500
|
|
73
170
|
|
|
74
171
|
|
|
75
|
-
@app.route(
|
|
172
|
+
@app.route("/save-data", methods=["POST"])
|
|
76
173
|
def save_data():
|
|
77
174
|
try:
|
|
78
175
|
data = request.get_json()
|
|
@@ -87,33 +184,36 @@ def save_data():
|
|
|
87
184
|
|
|
88
185
|
|
|
89
186
|
def inject_server_start_time(html_content, timestamp):
|
|
90
|
-
placeholder =
|
|
91
|
-
replacement = f
|
|
187
|
+
placeholder = "<script>"
|
|
188
|
+
replacement = f"<script>const serverStartTime = {timestamp};"
|
|
92
189
|
return html_content.replace(placeholder, replacement)
|
|
93
190
|
|
|
94
191
|
|
|
95
|
-
@app.route(
|
|
192
|
+
@app.route("/favicon.ico")
|
|
96
193
|
def favicon():
|
|
97
|
-
return send_from_directory(
|
|
98
|
-
|
|
194
|
+
return send_from_directory(
|
|
195
|
+
os.path.join(app.root_path, "static"),
|
|
196
|
+
"favicon.ico",
|
|
197
|
+
mimetype="image/vnd.microsoft.icon",
|
|
198
|
+
)
|
|
99
199
|
|
|
100
200
|
|
|
101
|
-
@app.route(
|
|
201
|
+
@app.route("/<path:filename>")
|
|
102
202
|
def serve_static(filename):
|
|
103
|
-
return send_from_directory(
|
|
203
|
+
return send_from_directory("pages", filename)
|
|
104
204
|
|
|
105
205
|
|
|
106
|
-
@app.route(
|
|
206
|
+
@app.route("/")
|
|
107
207
|
def index():
|
|
108
|
-
return send_from_directory(
|
|
208
|
+
return send_from_directory("templates", "index.html")
|
|
109
209
|
|
|
110
210
|
|
|
111
|
-
@app.route(
|
|
211
|
+
@app.route("/texthooker")
|
|
112
212
|
def texthooker():
|
|
113
|
-
return send_from_directory(
|
|
213
|
+
return send_from_directory("templates", "index.html")
|
|
114
214
|
|
|
115
215
|
|
|
116
|
-
@app.route(
|
|
216
|
+
@app.route("/textreplacements")
|
|
117
217
|
def textreplacements():
|
|
118
218
|
# Serve the text replacements data as JSON for compatibility
|
|
119
219
|
try:
|
|
@@ -125,48 +225,59 @@ def textreplacements():
|
|
|
125
225
|
except Exception as e:
|
|
126
226
|
return jsonify({"error": f"Failed to load text replacements: {str(e)}"}), 500
|
|
127
227
|
|
|
128
|
-
|
|
228
|
+
|
|
229
|
+
@app.route("/database")
|
|
129
230
|
def database():
|
|
130
|
-
return flask.render_template(
|
|
231
|
+
return flask.render_template("database.html")
|
|
131
232
|
|
|
132
233
|
|
|
133
|
-
@app.route(
|
|
234
|
+
@app.route("/data", methods=["GET"])
|
|
134
235
|
def get_data():
|
|
135
236
|
return jsonify([event.to_dict() for event in event_manager])
|
|
136
237
|
|
|
137
238
|
|
|
138
|
-
@app.route(
|
|
239
|
+
@app.route("/get_ids", methods=["GET"])
|
|
139
240
|
def get_ids():
|
|
140
241
|
asyncio.run(check_for_lines_outside_replay_buffer())
|
|
141
|
-
return jsonify(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
242
|
+
return jsonify(
|
|
243
|
+
{
|
|
244
|
+
"ids": list(event_manager.get_ids()),
|
|
245
|
+
"timed_out_ids": list(event_manager.timed_out_ids),
|
|
246
|
+
}
|
|
247
|
+
)
|
|
145
248
|
|
|
146
249
|
|
|
147
|
-
@app.route(
|
|
250
|
+
@app.route("/clear_history", methods=["POST"])
|
|
148
251
|
def clear_history():
|
|
149
252
|
temp_em = EventManager()
|
|
150
253
|
temp_em.clear_history()
|
|
151
254
|
temp_em.close_connection()
|
|
152
|
-
return jsonify({
|
|
255
|
+
return jsonify({"message": "History cleared successfully"}), 200
|
|
153
256
|
|
|
154
257
|
|
|
155
258
|
async def check_for_lines_outside_replay_buffer():
|
|
156
|
-
time_window =
|
|
259
|
+
time_window = (
|
|
260
|
+
datetime.datetime.now()
|
|
261
|
+
- datetime.timedelta(seconds=gsm_state.replay_buffer_length)
|
|
262
|
+
- datetime.timedelta(seconds=5)
|
|
263
|
+
)
|
|
157
264
|
# logger.info(f"Checking for lines outside replay buffer time window: {time_window}")
|
|
158
|
-
lines_outside_buffer = [
|
|
265
|
+
lines_outside_buffer = [
|
|
266
|
+
line.id for line in event_manager.get_events() if line.time < time_window
|
|
267
|
+
]
|
|
159
268
|
# logger.info(f"Lines outside replay buffer: {lines_outside_buffer}")
|
|
160
269
|
event_manager.remove_lines_by_ids(lines_outside_buffer, timed_out=True)
|
|
161
|
-
|
|
270
|
+
|
|
162
271
|
|
|
163
272
|
async def add_event_to_texthooker(line):
|
|
164
273
|
new_event = event_manager.add_gameline(line)
|
|
165
|
-
await websocket_server_thread.send_text(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
await websocket_server_thread.send_text(
|
|
275
|
+
{
|
|
276
|
+
"event": "text_received",
|
|
277
|
+
"sentence": line.text,
|
|
278
|
+
"data": new_event.to_serializable(),
|
|
279
|
+
}
|
|
280
|
+
)
|
|
170
281
|
if get_config().advanced.plaintext_websocket_port:
|
|
171
282
|
await plaintext_websocket_server_thread.send_text(line.text)
|
|
172
283
|
await check_for_lines_outside_replay_buffer()
|
|
@@ -177,61 +288,71 @@ async def send_word_coordinates_to_overlay(boxes):
|
|
|
177
288
|
await overlay_server_thread.send_text(boxes)
|
|
178
289
|
|
|
179
290
|
|
|
180
|
-
@app.route(
|
|
291
|
+
@app.route("/update_checkbox", methods=["POST"])
|
|
181
292
|
def update_event():
|
|
182
293
|
data = request.get_json()
|
|
183
|
-
event_id = data.get(
|
|
294
|
+
event_id = data.get("id")
|
|
184
295
|
|
|
185
296
|
if event_id is None:
|
|
186
|
-
return jsonify({
|
|
297
|
+
return jsonify({"error": "Missing id"}), 400
|
|
187
298
|
event = event_manager.get(event_id)
|
|
188
299
|
event_manager.get(event_id).checked = not event.checked
|
|
189
|
-
return jsonify({
|
|
300
|
+
return jsonify({"message": "Event updated successfully"}), 200
|
|
190
301
|
|
|
191
302
|
|
|
192
|
-
@app.route(
|
|
303
|
+
@app.route("/get-screenshot", methods=["Post"])
|
|
193
304
|
def get_screenshot():
|
|
194
305
|
"""Endpoint to get a screenshot of the current game screen."""
|
|
195
306
|
data = request.get_json()
|
|
196
|
-
event_id = data.get(
|
|
307
|
+
event_id = data.get("id")
|
|
197
308
|
if event_id is None:
|
|
198
|
-
return jsonify({
|
|
309
|
+
return jsonify({"error": "Missing id"}), 400
|
|
199
310
|
line = get_line_by_id(event_id)
|
|
200
311
|
if not line:
|
|
201
|
-
return jsonify({
|
|
312
|
+
return jsonify({"error": "Invalid id"}), 400
|
|
202
313
|
gsm_state.line_for_screenshot = line
|
|
203
|
-
if
|
|
314
|
+
if (
|
|
315
|
+
gsm_state.previous_line_for_screenshot
|
|
316
|
+
and gsm_state.line_for_screenshot == gsm_state.previous_line_for_screenshot
|
|
317
|
+
or gsm_state.previous_line_for_audio
|
|
318
|
+
and gsm_state.line_for_screenshot == gsm_state.previous_line_for_audio
|
|
319
|
+
):
|
|
204
320
|
handle_texthooker_button(gsm_state.previous_replay)
|
|
205
321
|
else:
|
|
206
322
|
obs.save_replay_buffer()
|
|
207
323
|
return jsonify({}), 200
|
|
208
324
|
|
|
209
325
|
|
|
210
|
-
@app.route(
|
|
326
|
+
@app.route("/play-audio", methods=["POST"])
|
|
211
327
|
def play_audio():
|
|
212
328
|
"""Endpoint to play audio for a specific event."""
|
|
213
329
|
data = request.get_json()
|
|
214
|
-
event_id = data.get(
|
|
330
|
+
event_id = data.get("id")
|
|
215
331
|
if event_id is None:
|
|
216
|
-
return jsonify({
|
|
332
|
+
return jsonify({"error": "Missing id"}), 400
|
|
217
333
|
print(f"Playing audio for event ID: {event_id}")
|
|
218
334
|
line = get_line_by_id(event_id)
|
|
219
335
|
if not line:
|
|
220
|
-
return jsonify({
|
|
336
|
+
return jsonify({"error": "Invalid id"}), 400
|
|
221
337
|
gsm_state.line_for_audio = line
|
|
222
338
|
print(f"gsm_state.line_for_audio: {gsm_state.line_for_audio}")
|
|
223
|
-
if
|
|
339
|
+
if (
|
|
340
|
+
gsm_state.previous_line_for_audio
|
|
341
|
+
and gsm_state.line_for_audio == gsm_state.previous_line_for_audio
|
|
342
|
+
or gsm_state.previous_line_for_screenshot
|
|
343
|
+
and gsm_state.line_for_audio == gsm_state.previous_line_for_screenshot
|
|
344
|
+
):
|
|
224
345
|
handle_texthooker_button(gsm_state.previous_replay)
|
|
225
346
|
else:
|
|
226
347
|
obs.save_replay_buffer()
|
|
227
348
|
return jsonify({}), 200
|
|
228
349
|
|
|
229
350
|
|
|
230
|
-
@app.route("/translate-line", methods=[
|
|
351
|
+
@app.route("/translate-line", methods=["POST"])
|
|
231
352
|
def translate_line():
|
|
232
353
|
data = request.get_json()
|
|
233
|
-
event_id = data.get(
|
|
234
|
-
text = data.get(
|
|
354
|
+
event_id = data.get("id")
|
|
355
|
+
text = data.get("text", "").strip()
|
|
235
356
|
if event_id is None:
|
|
236
357
|
return jsonify({'error': 'Missing id'}), 400
|
|
237
358
|
|
|
@@ -254,34 +375,46 @@ def translate_line():
|
|
|
254
375
|
"""
|
|
255
376
|
|
|
256
377
|
if not get_config().ai.is_configured():
|
|
257
|
-
return jsonify(
|
|
378
|
+
return jsonify(
|
|
379
|
+
{
|
|
380
|
+
"error": 'AI translation is not properly configured. Please check your settings in the "AI" Tab.'
|
|
381
|
+
}
|
|
382
|
+
), 400
|
|
258
383
|
line = get_line_by_id(event_id)
|
|
259
384
|
if line is None:
|
|
260
|
-
return jsonify({
|
|
385
|
+
return jsonify({"error": "Invalid id"}), 400
|
|
261
386
|
line_to_translate = text if text else line.text
|
|
262
387
|
translation = get_ai_prompt_result(
|
|
263
388
|
get_all_lines(), line_to_translate, line, get_current_game(), custom_prompt=prompt
|
|
264
389
|
)
|
|
265
390
|
line.set_TL(translation)
|
|
266
|
-
return jsonify({
|
|
391
|
+
return jsonify({"TL": translation}), 200
|
|
392
|
+
|
|
267
393
|
|
|
268
|
-
@app.route(
|
|
394
|
+
@app.route("/translate-multiple", methods=["POST"])
|
|
269
395
|
def translate_multiple():
|
|
270
396
|
data = request.get_json()
|
|
271
|
-
event_ids = data.get(
|
|
397
|
+
event_ids = data.get("ids", [])
|
|
272
398
|
if not event_ids:
|
|
273
|
-
return jsonify({
|
|
274
|
-
|
|
275
|
-
if not get_config().ai.is_configured():
|
|
276
|
-
return jsonify({'error': 'AI translation is not properly configured. Please check your settings in the "AI" Tab.'}), 400
|
|
399
|
+
return jsonify({"error": "Missing ids"}), 400
|
|
277
400
|
|
|
278
|
-
|
|
401
|
+
if not get_config().ai.is_configured():
|
|
402
|
+
return jsonify(
|
|
403
|
+
{
|
|
404
|
+
"error": 'AI translation is not properly configured. Please check your settings in the "AI" Tab.'
|
|
405
|
+
}
|
|
406
|
+
), 400
|
|
407
|
+
|
|
408
|
+
lines = [
|
|
409
|
+
get_line_by_id(event_id)
|
|
410
|
+
for event_id in event_ids
|
|
411
|
+
if get_line_by_id(event_id) is not None
|
|
412
|
+
]
|
|
279
413
|
|
|
280
414
|
text = "\n".join(line.text for line in lines)
|
|
281
415
|
|
|
282
416
|
language = get_config().general.get_native_language_name() if get_config().general.native_language else "English"
|
|
283
417
|
|
|
284
|
-
|
|
285
418
|
translate_multiple_lines_prompt = f"""
|
|
286
419
|
**Professional Game Localization Task**
|
|
287
420
|
Translate the following lines of game dialogue into natural-sounding, context-aware {language}:
|
|
@@ -296,62 +429,87 @@ def translate_multiple():
|
|
|
296
429
|
**Lines to Translate:**
|
|
297
430
|
"""
|
|
298
431
|
|
|
299
|
-
translation = get_ai_prompt_result(
|
|
300
|
-
|
|
432
|
+
translation = get_ai_prompt_result(
|
|
433
|
+
get_all_lines(),
|
|
434
|
+
text,
|
|
435
|
+
lines[0],
|
|
436
|
+
get_current_game(),
|
|
437
|
+
custom_prompt=translate_multiple_lines_prompt,
|
|
438
|
+
)
|
|
301
439
|
|
|
302
440
|
return translation, 200
|
|
303
441
|
|
|
304
|
-
|
|
442
|
+
|
|
443
|
+
@app.route("/get_status", methods=["GET"])
|
|
305
444
|
def get_status():
|
|
306
445
|
return jsonify(gsm_status.to_dict()), 200
|
|
307
446
|
|
|
308
|
-
|
|
309
|
-
|
|
447
|
+
|
|
448
|
+
@app.template_filter("datetimeformat")
|
|
449
|
+
def datetimeformat(value, format="%Y-%m-%d %H:%M:%S"):
|
|
310
450
|
"""Formats a timestamp into a human-readable string."""
|
|
311
451
|
if value is None:
|
|
312
452
|
return ""
|
|
313
453
|
return datetime.datetime.fromtimestamp(float(value)).strftime(format)
|
|
314
454
|
|
|
315
455
|
|
|
316
|
-
@app.route(
|
|
456
|
+
@app.route("/overview")
|
|
317
457
|
def overview():
|
|
318
458
|
"""Renders the overview page."""
|
|
319
459
|
from GameSentenceMiner.util.configuration import get_master_config, get_stats_config
|
|
320
|
-
return render_template('overview.html',
|
|
321
|
-
config=get_config(),
|
|
322
|
-
master_config=get_master_config(),
|
|
323
|
-
stats_config=get_stats_config())
|
|
324
460
|
|
|
325
|
-
|
|
461
|
+
return render_template(
|
|
462
|
+
"overview.html",
|
|
463
|
+
config=get_config(),
|
|
464
|
+
master_config=get_master_config(),
|
|
465
|
+
stats_config=get_stats_config(),
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.route("/stats")
|
|
326
470
|
def stats():
|
|
327
471
|
"""Renders the stats page."""
|
|
328
472
|
from GameSentenceMiner.util.configuration import get_master_config, get_stats_config
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
473
|
+
from GameSentenceMiner.util.stats_rollup_table import StatsRollupTable
|
|
474
|
+
|
|
475
|
+
# Get first date from rollup table to avoid extra API call on page load
|
|
476
|
+
first_rollup_date = StatsRollupTable.get_first_date()
|
|
477
|
+
|
|
478
|
+
return render_template(
|
|
479
|
+
"stats.html",
|
|
480
|
+
config=get_config(),
|
|
481
|
+
master_config=get_master_config(),
|
|
482
|
+
stats_config=get_stats_config(),
|
|
483
|
+
first_rollup_date=first_rollup_date,
|
|
484
|
+
)
|
|
333
485
|
|
|
334
|
-
|
|
486
|
+
|
|
487
|
+
@app.route("/goals")
|
|
335
488
|
def goals():
|
|
336
489
|
"""Renders the goals page."""
|
|
337
490
|
from GameSentenceMiner.util.configuration import get_master_config, get_stats_config
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
491
|
+
|
|
492
|
+
return render_template(
|
|
493
|
+
"goals.html",
|
|
494
|
+
config=get_config(),
|
|
495
|
+
master_config=get_master_config(),
|
|
496
|
+
stats_config=get_stats_config(),
|
|
497
|
+
)
|
|
342
498
|
|
|
343
499
|
|
|
344
|
-
@app.route(
|
|
500
|
+
@app.route("/search")
|
|
345
501
|
def search():
|
|
346
502
|
"""Renders the search page."""
|
|
347
|
-
return render_template(
|
|
503
|
+
return render_template("search.html")
|
|
348
504
|
|
|
349
|
-
|
|
505
|
+
|
|
506
|
+
@app.route("/anki_stats")
|
|
350
507
|
def anki_stats():
|
|
351
508
|
"""Renders the Anki statistics page."""
|
|
352
|
-
return render_template(
|
|
509
|
+
return render_template("anki_stats.html")
|
|
510
|
+
|
|
353
511
|
|
|
354
|
-
@app.route(
|
|
512
|
+
@app.route("/get_websocket_port", methods=["GET"])
|
|
355
513
|
def get_websocket_port():
|
|
356
514
|
return jsonify({"port": websocket_server_thread.get_ws_port_func()}), 200
|
|
357
515
|
|
|
@@ -366,28 +524,36 @@ def are_lines_selected():
|
|
|
366
524
|
|
|
367
525
|
def reset_checked_lines():
|
|
368
526
|
async def send_reset_message():
|
|
369
|
-
await websocket_server_thread.send_text(
|
|
370
|
-
|
|
371
|
-
|
|
527
|
+
await websocket_server_thread.send_text(
|
|
528
|
+
{
|
|
529
|
+
"event": "reset_checkboxes",
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
|
|
372
533
|
event_manager.reset_checked_lines()
|
|
373
534
|
asyncio.run(send_reset_message())
|
|
374
|
-
|
|
535
|
+
|
|
536
|
+
|
|
375
537
|
def reset_buttons():
|
|
376
538
|
async def send_reset_message():
|
|
377
|
-
await websocket_server_thread.send_text(
|
|
378
|
-
|
|
379
|
-
|
|
539
|
+
await websocket_server_thread.send_text(
|
|
540
|
+
{
|
|
541
|
+
"event": "reset_buttons",
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
|
|
380
545
|
asyncio.run(send_reset_message())
|
|
381
546
|
|
|
382
547
|
|
|
383
548
|
def open_texthooker():
|
|
384
|
-
webbrowser.open(url +
|
|
549
|
+
webbrowser.open(url + "/texthooker")
|
|
385
550
|
|
|
386
551
|
|
|
387
552
|
def start_web_server():
|
|
388
553
|
logger.debug("Starting web server...")
|
|
389
554
|
import logging
|
|
390
|
-
|
|
555
|
+
|
|
556
|
+
log = logging.getLogger("werkzeug")
|
|
391
557
|
log.setLevel(logging.ERROR) # Set to ERROR to suppress most logs
|
|
392
558
|
|
|
393
559
|
# Open the default browser
|
|
@@ -401,7 +567,10 @@ def start_web_server():
|
|
|
401
567
|
|
|
402
568
|
|
|
403
569
|
async def texthooker_page_coro():
|
|
404
|
-
global
|
|
570
|
+
global \
|
|
571
|
+
websocket_server_thread, \
|
|
572
|
+
plaintext_websocket_server_thread, \
|
|
573
|
+
overlay_server_thread
|
|
405
574
|
# Run the WebSocket server in the asyncio event loop
|
|
406
575
|
flask_thread = threading.Thread(target=start_web_server)
|
|
407
576
|
flask_thread.daemon = True
|
|
@@ -417,5 +586,5 @@ def run_text_hooker_page():
|
|
|
417
586
|
logger.info("Shutting down due to KeyboardInterrupt.")
|
|
418
587
|
|
|
419
588
|
|
|
420
|
-
if __name__ ==
|
|
421
|
-
asyncio.run(texthooker_page_coro())
|
|
589
|
+
if __name__ == "__main__":
|
|
590
|
+
asyncio.run(texthooker_page_coro())
|