GameSentenceMiner 2.16.11__py3-none-any.whl → 2.16.13__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.
- GameSentenceMiner/config_gui.py +1 -1
- GameSentenceMiner/util/get_overlay_coords.py +78 -11
- GameSentenceMiner/web/static/js/stats.js +61 -22
- GameSentenceMiner/web/stats.py +9 -4
- GameSentenceMiner/web/templates/stats.html +22 -0
- GameSentenceMiner/web/texthooking_page.py +5 -1
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/RECORD +12 -12
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/top_level.txt +0 -0
GameSentenceMiner/config_gui.py
CHANGED
|
@@ -417,10 +417,10 @@ class ConfigApp:
|
|
|
417
417
|
self.create_screenshot_tab()
|
|
418
418
|
self.create_audio_tab()
|
|
419
419
|
self.create_obs_tab()
|
|
420
|
-
self.create_profiles_tab()
|
|
421
420
|
self.create_ai_tab()
|
|
422
421
|
self.create_advanced_tab()
|
|
423
422
|
self.create_overlay_tab()
|
|
423
|
+
self.create_profiles_tab()
|
|
424
424
|
# self.create_wip_tab()
|
|
425
425
|
|
|
426
426
|
def add_reset_button(self, frame, category, row, column=0, recreate_tab=None):
|
|
@@ -9,11 +9,11 @@ import time
|
|
|
9
9
|
from PIL import Image
|
|
10
10
|
from typing import Dict, Any, List, Tuple
|
|
11
11
|
import json
|
|
12
|
-
from rapidfuzz
|
|
12
|
+
from rapidfuzz import fuzz
|
|
13
13
|
|
|
14
14
|
# Local application imports
|
|
15
15
|
from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness
|
|
16
|
-
from GameSentenceMiner.util.configuration import OverlayEngine, get_config, is_windows, is_beangate, logger
|
|
16
|
+
from GameSentenceMiner.util.configuration import OverlayEngine, get_config, get_temporary_directory, is_windows, is_beangate, logger
|
|
17
17
|
from GameSentenceMiner.util.electron_config import get_ocr_language
|
|
18
18
|
from GameSentenceMiner.obs import get_screenshot_PIL
|
|
19
19
|
from GameSentenceMiner.web.texthooking_page import send_word_coordinates_to_overlay
|
|
@@ -93,7 +93,8 @@ class OverlayThread(threading.Thread):
|
|
|
93
93
|
super().__init__()
|
|
94
94
|
self.overlay_processor = OverlayProcessor()
|
|
95
95
|
self.loop = asyncio.new_event_loop()
|
|
96
|
-
self.daemon = True
|
|
96
|
+
self.daemon = True
|
|
97
|
+
self.first_time_run = True
|
|
97
98
|
|
|
98
99
|
def run(self):
|
|
99
100
|
"""Runs the overlay processing loop."""
|
|
@@ -103,11 +104,18 @@ class OverlayThread(threading.Thread):
|
|
|
103
104
|
async def overlay_loop(self):
|
|
104
105
|
"""Main loop to periodically process and send overlay data."""
|
|
105
106
|
while True:
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if overlay_server_thread.has_clients():
|
|
108
|
+
if get_config().overlay.periodic:
|
|
109
|
+
await self.overlay_processor.find_box_and_send_to_overlay('')
|
|
110
|
+
await asyncio.sleep(get_config().overlay.periodic_interval)
|
|
111
|
+
elif self.first_time_run:
|
|
112
|
+
await self.overlay_processor.find_box_and_send_to_overlay('')
|
|
113
|
+
self.first_time_run = False
|
|
114
|
+
else:
|
|
115
|
+
await asyncio.sleep(3)
|
|
109
116
|
else:
|
|
110
|
-
|
|
117
|
+
self.first_time_run = True
|
|
118
|
+
await asyncio.sleep(3)
|
|
111
119
|
|
|
112
120
|
class OverlayProcessor:
|
|
113
121
|
"""
|
|
@@ -125,6 +133,8 @@ class OverlayProcessor:
|
|
|
125
133
|
self.lens = None
|
|
126
134
|
self.regex = None
|
|
127
135
|
self.ready = False
|
|
136
|
+
self.last_oneocr_result = None
|
|
137
|
+
self.last_lens_result = None
|
|
128
138
|
|
|
129
139
|
try:
|
|
130
140
|
if self.config.overlay.websocket_port and all([GoogleLens, get_regex]):
|
|
@@ -221,6 +231,8 @@ class OverlayProcessor:
|
|
|
221
231
|
monitor = self.get_monitor_workarea(0) # Get primary monitor work area
|
|
222
232
|
sct_img = sct.grab(monitor)
|
|
223
233
|
img = Image.frombytes('RGB', sct_img.size, sct_img.bgra, 'raw', 'BGRX')
|
|
234
|
+
|
|
235
|
+
img.save(os.path.join(get_temporary_directory(), "latest_overlay_screenshot.png"))
|
|
224
236
|
|
|
225
237
|
return img, monitor['width'], monitor['height']
|
|
226
238
|
|
|
@@ -261,6 +273,8 @@ class OverlayProcessor:
|
|
|
261
273
|
paste_x = math.floor(x1)
|
|
262
274
|
paste_y = math.floor(y1)
|
|
263
275
|
composite_img.paste(cropped_image, (paste_x, paste_y))
|
|
276
|
+
|
|
277
|
+
composite_img.save(os.path.join(get_temporary_directory(), "latest_overlay_screenshot_trimmed.png"))
|
|
264
278
|
|
|
265
279
|
return composite_img
|
|
266
280
|
|
|
@@ -277,7 +291,7 @@ class OverlayProcessor:
|
|
|
277
291
|
return []
|
|
278
292
|
if self.oneocr:
|
|
279
293
|
# 2. Use OneOCR to find general text areas (fast)
|
|
280
|
-
|
|
294
|
+
res, text, oneocr_results, crop_coords_list = self.oneocr(
|
|
281
295
|
full_screenshot,
|
|
282
296
|
return_coords=True,
|
|
283
297
|
multiple_crop_coords=True,
|
|
@@ -285,8 +299,19 @@ class OverlayProcessor:
|
|
|
285
299
|
furigana_filter_sensitivity=None, # Disable furigana filtering
|
|
286
300
|
)
|
|
287
301
|
|
|
302
|
+
text_str = "".join([text for text in text if self.regex.match(text)])
|
|
303
|
+
|
|
304
|
+
# RapidFuzz fuzzy match 90% to not send the same results repeatedly
|
|
305
|
+
if self.last_oneocr_result:
|
|
306
|
+
|
|
307
|
+
score = fuzz.ratio(text_str, self.last_oneocr_result)
|
|
308
|
+
if score >= 80:
|
|
309
|
+
logger.info("OneOCR results are similar to the last results (score: %d). Skipping overlay update.", score)
|
|
310
|
+
return
|
|
311
|
+
self.last_oneocr_result = text_str
|
|
312
|
+
|
|
288
313
|
logger.info("Sending OneOCR results to overlay.")
|
|
289
|
-
await send_word_coordinates_to_overlay(oneocr_results)
|
|
314
|
+
await send_word_coordinates_to_overlay(self._convert_oneocr_results_to_percentages(oneocr_results, monitor_width, monitor_height))
|
|
290
315
|
|
|
291
316
|
# If User Home is beangate
|
|
292
317
|
if is_beangate:
|
|
@@ -317,9 +342,19 @@ class OverlayProcessor:
|
|
|
317
342
|
if len(res) != 3:
|
|
318
343
|
return
|
|
319
344
|
|
|
320
|
-
|
|
345
|
+
success, text_list, coords = res
|
|
346
|
+
|
|
347
|
+
text_str = "".join([text for text in text_list if self.regex.match(text)])
|
|
348
|
+
|
|
349
|
+
# RapidFuzz fuzzy match 90% to not send the same results repeatedly
|
|
350
|
+
if self.last_lens_result:
|
|
351
|
+
score = fuzz.ratio(text_str, self.last_lens_result)
|
|
352
|
+
if score >= 80:
|
|
353
|
+
logger.info("Google Lens results are similar to the last results (score: %d). Skipping overlay update.", score)
|
|
354
|
+
return
|
|
355
|
+
self.last_lens_result = text_str
|
|
321
356
|
|
|
322
|
-
if not
|
|
357
|
+
if not success or not coords:
|
|
323
358
|
return
|
|
324
359
|
|
|
325
360
|
# 5. Process the high-accuracy results into the desired format
|
|
@@ -433,6 +468,38 @@ class OverlayProcessor:
|
|
|
433
468
|
"x3": center_x + half_w, "y3": center_y + half_h,
|
|
434
469
|
"x4": center_x - half_w, "y4": center_y + half_h,
|
|
435
470
|
}
|
|
471
|
+
|
|
472
|
+
def _convert_oneocr_results_to_percentages(
|
|
473
|
+
self,
|
|
474
|
+
oneocr_results: List[Dict[str, Any]],
|
|
475
|
+
monitor_width: int,
|
|
476
|
+
monitor_height: int
|
|
477
|
+
) -> List[Dict[str, Any]]:
|
|
478
|
+
"""
|
|
479
|
+
Converts OneOCR results with pixel coordinates to percentages relative to the monitor size.
|
|
480
|
+
"""
|
|
481
|
+
converted_results = []
|
|
482
|
+
for item in oneocr_results:
|
|
483
|
+
bbox = item.get("bounding_rect", {})
|
|
484
|
+
if not bbox:
|
|
485
|
+
continue
|
|
486
|
+
# Convert each coordinate to a percentage of the monitor dimensions
|
|
487
|
+
converted_bbox = {
|
|
488
|
+
key: (value / monitor_width if "x" in key else value / monitor_height)
|
|
489
|
+
for key, value in bbox.items()
|
|
490
|
+
}
|
|
491
|
+
converted_item = item.copy()
|
|
492
|
+
converted_item["bounding_rect"] = converted_bbox
|
|
493
|
+
converted_results.append(converted_item)
|
|
494
|
+
for word in converted_item.get("words", []):
|
|
495
|
+
word_bbox = word.get("bounding_rect", {})
|
|
496
|
+
if word_bbox:
|
|
497
|
+
word["bounding_rect"] = {
|
|
498
|
+
key: (value / monitor_width if "x" in key else value / monitor_height)
|
|
499
|
+
for key, value in word_bbox.items()
|
|
500
|
+
}
|
|
501
|
+
# logger.info(f"Converted OneOCR results to percentages: {converted_results}")
|
|
502
|
+
return converted_results
|
|
436
503
|
|
|
437
504
|
async def main_test_screenshot():
|
|
438
505
|
"""
|
|
@@ -152,11 +152,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
152
152
|
tempStreak = 0;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
|
|
156
|
-
// Calculate current streak from today backwards
|
|
155
|
+
|
|
156
|
+
// Calculate current streak from today backwards, using streak requirement hours from config
|
|
157
157
|
const date = new Date();
|
|
158
158
|
const today = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
159
|
-
|
|
159
|
+
const streakRequirement = window.statsConfig ? window.statsConfig.streakRequirementHours : 1.0;
|
|
160
|
+
|
|
160
161
|
// Find today's index or the most recent date before today
|
|
161
162
|
let todayIndex = -1;
|
|
162
163
|
for (let i = dates.length - 1; i >= 0; i--) {
|
|
@@ -165,11 +166,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
165
166
|
break;
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
|
|
169
170
|
// Count backwards from today (or most recent date)
|
|
170
171
|
if (todayIndex >= 0) {
|
|
171
172
|
for (let i = todayIndex; i >= 0; i--) {
|
|
172
|
-
if (dates[i].activity
|
|
173
|
+
if (dates[i].activity >= streakRequirement) {
|
|
173
174
|
currentStreak++;
|
|
174
175
|
} else {
|
|
175
176
|
break;
|
|
@@ -196,18 +197,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
196
197
|
// Calculate reading time for each day with activity
|
|
197
198
|
let totalHours = 0;
|
|
198
199
|
let activeDays = 0;
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
201
|
+
// Try to get AFK timer from settings modal if available and valid
|
|
202
|
+
const afkTimerInput = document.getElementById('afkTimer');
|
|
203
|
+
if (afkTimerInput && afkTimerInput.value) {
|
|
204
|
+
const parsed = parseInt(afkTimerInput.value, 10);
|
|
205
|
+
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
206
|
+
}
|
|
207
|
+
|
|
201
208
|
for (const [dateStr, timestamps] of Object.entries(dailyTimestamps)) {
|
|
202
209
|
if (timestamps.length >= 2) {
|
|
203
210
|
timestamps.sort((a, b) => a - b);
|
|
204
211
|
let dayReadingTime = 0;
|
|
205
|
-
|
|
212
|
+
|
|
206
213
|
for (let i = 1; i < timestamps.length; i++) {
|
|
207
214
|
const gap = timestamps[i] - timestamps[i-1];
|
|
208
215
|
dayReadingTime += Math.min(gap, afkTimerSeconds);
|
|
209
216
|
}
|
|
210
|
-
|
|
217
|
+
|
|
211
218
|
if (dayReadingTime > 0) {
|
|
212
219
|
totalHours += dayReadingTime / 3600;
|
|
213
220
|
activeDays++;
|
|
@@ -837,14 +844,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
837
844
|
}
|
|
838
845
|
|
|
839
846
|
// Goal Progress Chart functionality
|
|
840
|
-
let goalSettings = {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
};
|
|
847
|
+
let goalSettings = window.statsConfig || {};
|
|
848
|
+
if (!goalSettings.reading_hours_target) goalSettings.reading_hours_target = 1500;
|
|
849
|
+
if (!goalSettings.character_count_target) goalSettings.character_count_target = 25000000;
|
|
850
|
+
if (!goalSettings.games_target) goalSettings.games_target = 100;
|
|
845
851
|
|
|
846
|
-
// Function to load goal settings from API
|
|
852
|
+
// Function to load goal settings from API (fallback)
|
|
847
853
|
async function loadGoalSettings() {
|
|
854
|
+
// Use global config if available, otherwise fetch
|
|
855
|
+
if (window.statsConfig) {
|
|
856
|
+
goalSettings.reading_hours_target = window.statsConfig.readingHoursTarget || 1500;
|
|
857
|
+
goalSettings.character_count_target = window.statsConfig.characterCountTarget || 25000000;
|
|
858
|
+
goalSettings.games_target = window.statsConfig.gamesTarget || 100;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
848
861
|
try {
|
|
849
862
|
const response = await fetch('/api/settings');
|
|
850
863
|
if (response.ok) {
|
|
@@ -899,8 +912,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
899
912
|
if (timestamps.length >= 2) {
|
|
900
913
|
timestamps.sort((a, b) => a - b);
|
|
901
914
|
let dayHours = 0;
|
|
902
|
-
|
|
903
|
-
|
|
915
|
+
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
916
|
+
// Try to get AFK timer from settings modal if available and valid
|
|
917
|
+
const afkTimerInput = document.getElementById('afkTimer');
|
|
918
|
+
if (afkTimerInput && afkTimerInput.value) {
|
|
919
|
+
const parsed = parseInt(afkTimerInput.value, 10);
|
|
920
|
+
if (!isNaN(parsed) && parsed > 0) afkTimerSeconds = parsed;
|
|
921
|
+
}
|
|
922
|
+
|
|
904
923
|
for (let i = 1; i < timestamps.length; i++) {
|
|
905
924
|
const gap = timestamps[i] - timestamps[i-1];
|
|
906
925
|
dayHours += Math.min(gap, afkTimerSeconds) / 3600;
|
|
@@ -1099,9 +1118,30 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1099
1118
|
}
|
|
1100
1119
|
|
|
1101
1120
|
// Initial load with saved year preference
|
|
1102
|
-
const savedYear = localStorage.getItem('selectedHeatmapYear') || 'all';
|
|
1121
|
+
const savedYear = localStorage.getItem('selectedHeatmapYear') || window.statsConfig?.heatmapDisplayYear || 'all';
|
|
1103
1122
|
loadStatsData(savedYear);
|
|
1104
1123
|
|
|
1124
|
+
// Populate settings modal with global config values on load
|
|
1125
|
+
if (window.statsConfig) {
|
|
1126
|
+
const sessionGapInput = document.getElementById('sessionGap');
|
|
1127
|
+
if (sessionGapInput) sessionGapInput.value = window.statsConfig.sessionGapSeconds || 3600;
|
|
1128
|
+
|
|
1129
|
+
const streakReqInput = document.getElementById('streakRequirement');
|
|
1130
|
+
if (streakReqInput) streakReqInput.value = window.statsConfig.streakRequirementHours || 1.0;
|
|
1131
|
+
|
|
1132
|
+
const heatmapYearSelect = document.getElementById('heatmapYear');
|
|
1133
|
+
if (heatmapYearSelect) heatmapYearSelect.value = window.statsConfig.heatmapDisplayYear || 'all';
|
|
1134
|
+
|
|
1135
|
+
const hoursTargetInput = document.getElementById('readingHoursTarget');
|
|
1136
|
+
if (hoursTargetInput) hoursTargetInput.value = window.statsConfig.readingHoursTarget || 1500;
|
|
1137
|
+
|
|
1138
|
+
const charsTargetInput = document.getElementById('characterCountTarget');
|
|
1139
|
+
if (charsTargetInput) charsTargetInput.value = window.statsConfig.characterCountTarget || 25000000;
|
|
1140
|
+
|
|
1141
|
+
const gamesTargetInput = document.getElementById('gamesTarget');
|
|
1142
|
+
if (gamesTargetInput) gamesTargetInput.value = window.statsConfig.gamesTarget || 100;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1105
1145
|
// Function to update goal progress using existing stats data
|
|
1106
1146
|
async function updateGoalProgressWithData(statsData) {
|
|
1107
1147
|
const goalProgressChart = document.getElementById('goalProgressChart');
|
|
@@ -1178,9 +1218,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1178
1218
|
|
|
1179
1219
|
// Calculate sessions (count gaps > session threshold as new sessions)
|
|
1180
1220
|
let sessions = 0;
|
|
1181
|
-
|
|
1182
|
-
// Try to get session gap from settings modal if available
|
|
1183
|
-
let sessionGap = sessionGapDefault;
|
|
1221
|
+
let sessionGap = window.statsConfig ? window.statsConfig.sessionGapSeconds : 3600;
|
|
1222
|
+
// Try to get session gap from settings modal if available and valid
|
|
1184
1223
|
const sessionGapInput = document.getElementById('sessionGap');
|
|
1185
1224
|
if (sessionGapInput && sessionGapInput.value) {
|
|
1186
1225
|
const parsed = parseInt(sessionGapInput.value, 10);
|
|
@@ -1214,7 +1253,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1214
1253
|
.filter(ts => !isNaN(ts))
|
|
1215
1254
|
.sort((a, b) => a - b);
|
|
1216
1255
|
// Get AFK timer from settings modal if available
|
|
1217
|
-
let afkTimerSeconds = 120;
|
|
1256
|
+
let afkTimerSeconds = window.statsConfig ? window.statsConfig.afkTimerSeconds : 120;
|
|
1218
1257
|
const afkTimerInput = document.getElementById('afkTimer');
|
|
1219
1258
|
if (afkTimerInput && afkTimerInput.value) {
|
|
1220
1259
|
const parsed = parseInt(afkTimerInput.value, 10);
|
GameSentenceMiner/web/stats.py
CHANGED
|
@@ -345,8 +345,11 @@ def calculate_time_based_streak(lines, streak_requirement_hours=None):
|
|
|
345
345
|
int: Current streak in days
|
|
346
346
|
"""
|
|
347
347
|
if streak_requirement_hours is None:
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
# Prefer stats_config if available, fallback to config.advanced, then 1.0
|
|
349
|
+
try:
|
|
350
|
+
streak_requirement_hours = get_stats_config().streak_requirement_hours
|
|
351
|
+
except AttributeError:
|
|
352
|
+
streak_requirement_hours = getattr(get_config().advanced, 'streak_requirement_hours', 1.0)
|
|
350
353
|
# Add debug logging
|
|
351
354
|
logger.debug(f"Calculating streak with requirement: {streak_requirement_hours} hours")
|
|
352
355
|
logger.debug(f"Processing {len(lines)} lines for streak calculation")
|
|
@@ -440,9 +443,10 @@ def calculate_current_game_stats(all_lines):
|
|
|
440
443
|
# Calculate sessions (gaps of more than session_gap_seconds = new session)
|
|
441
444
|
sorted_timestamps = sorted(timestamps)
|
|
442
445
|
sessions = 1
|
|
446
|
+
session_gap = get_stats_config().session_gap_seconds
|
|
443
447
|
for i in range(1, len(sorted_timestamps)):
|
|
444
448
|
time_gap = sorted_timestamps[i] - sorted_timestamps[i-1]
|
|
445
|
-
if time_gap >
|
|
449
|
+
if time_gap > session_gap:
|
|
446
450
|
sessions += 1
|
|
447
451
|
|
|
448
452
|
# Calculate daily activity for progress trend
|
|
@@ -533,9 +537,10 @@ def calculate_all_games_stats(all_lines):
|
|
|
533
537
|
# Calculate sessions across all games (gaps of more than 1 hour = new session)
|
|
534
538
|
sorted_timestamps = sorted(timestamps)
|
|
535
539
|
sessions = 1
|
|
540
|
+
session_gap = get_stats_config().session_gap_seconds
|
|
536
541
|
for i in range(1, len(sorted_timestamps)):
|
|
537
542
|
time_gap = sorted_timestamps[i] - sorted_timestamps[i-1]
|
|
538
|
-
if time_gap >
|
|
543
|
+
if time_gap > session_gap:
|
|
539
544
|
sessions += 1
|
|
540
545
|
|
|
541
546
|
# Calculate daily activity for progress trend
|
|
@@ -520,6 +520,28 @@
|
|
|
520
520
|
</div>
|
|
521
521
|
|
|
522
522
|
|
|
523
|
+
{% set afk_timer = stats_config.afk_timer_seconds | default(120) %}
|
|
524
|
+
{% set session_gap = stats_config.session_gap_seconds | default(3600) %}
|
|
525
|
+
{% set streak_req = config.advanced.streak_requirement_hours | default(1.0) %}
|
|
526
|
+
{% set hours_target = stats_config.reading_hours_target | default(1500) %}
|
|
527
|
+
{% set chars_target = stats_config.character_count_target | default(25000000) %}
|
|
528
|
+
{% set games_target = stats_config.games_target | default(100) %}
|
|
529
|
+
{% set heatmap_year = 'all' %}
|
|
530
|
+
|
|
531
|
+
{% set stats_config = {
|
|
532
|
+
'afkTimerSeconds': afk_timer,
|
|
533
|
+
'sessionGapSeconds': session_gap,
|
|
534
|
+
'streakRequirementHours': streak_req,
|
|
535
|
+
'readingHoursTarget': hours_target,
|
|
536
|
+
'characterCountTarget': chars_target,
|
|
537
|
+
'gamesTarget': games_target,
|
|
538
|
+
'heatmapDisplayYear': heatmap_year
|
|
539
|
+
} %}
|
|
540
|
+
|
|
541
|
+
<!-- Inject stats config values for frontend -->
|
|
542
|
+
<script>
|
|
543
|
+
window.statsConfig = {{ stats_config | tojson }};
|
|
544
|
+
</script>
|
|
523
545
|
<!-- Include shared JavaScript first (required dependency for stats.js) -->
|
|
524
546
|
<script src="/static/js/shared.js"></script>
|
|
525
547
|
<script src="/static/js/kanji-grid.js"></script>
|
|
@@ -261,7 +261,11 @@ def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
|
|
|
261
261
|
@app.route('/stats')
|
|
262
262
|
def stats():
|
|
263
263
|
"""Renders the stats page."""
|
|
264
|
-
|
|
264
|
+
from GameSentenceMiner.util.configuration import get_master_config, get_stats_config
|
|
265
|
+
return render_template('stats.html',
|
|
266
|
+
config=get_config(),
|
|
267
|
+
master_config=get_master_config(),
|
|
268
|
+
stats_config=get_stats_config())
|
|
265
269
|
|
|
266
270
|
@app.route('/api/anki_stats')
|
|
267
271
|
def api_anki_stats():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
GameSentenceMiner/anki.py,sha256=Qq03nxYCA0bXS8IR1vnEB9cv2vxo6Ruy-UuiojC4ad0,26518
|
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=
|
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=kyAK3s1tkYmjO_zdJDYWR0rubAsS0vHiT46HdMTxJGk,146121
|
|
4
4
|
GameSentenceMiner/gametext.py,sha256=fgBgLchezpauWELE9Y5G3kVCLfAneD0X4lJFoI3FYbs,10351
|
|
5
5
|
GameSentenceMiner/gsm.py,sha256=1eq5nkYulfm85749g8g2s_WkqqiQWDopUXyimJLIy6M,33814
|
|
6
6
|
GameSentenceMiner/obs.py,sha256=EyAYhaLvMjoeC-3j7fuvkqZN5logFFanPfb8Wn1C6m0,27296
|
|
@@ -41,7 +41,7 @@ GameSentenceMiner/util/configuration.py,sha256=qATOwZahVQNP8-ZnWiAKuR7UJLW25QNDm
|
|
|
41
41
|
GameSentenceMiner/util/db.py,sha256=B2Qwg7i0Qn_yxov-NhcT9QdFkF218Cqea_V7ZPzYBzM,21365
|
|
42
42
|
GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
|
|
43
43
|
GameSentenceMiner/util/ffmpeg.py,sha256=g3v1aJnv3qWekDnO0FdYozB-MG9di4WUvPA3NyXY9Ws,28998
|
|
44
|
-
GameSentenceMiner/util/get_overlay_coords.py,sha256=
|
|
44
|
+
GameSentenceMiner/util/get_overlay_coords.py,sha256=lUD2YU4iVoXRHvQzkCN0Re3IQkYKemU2NV81MtQ94Uk,22028
|
|
45
45
|
GameSentenceMiner/util/gsm_utils.py,sha256=Piwv88Q9av2LBeN7M6QDi0Mp0_R2lNbkcI6ekK5hd2o,11851
|
|
46
46
|
GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
|
|
47
47
|
GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
|
|
@@ -60,8 +60,8 @@ GameSentenceMiner/web/database_api.py,sha256=XkFJDVm9X15Coz6rakNLKQj-218MYziGOeo
|
|
|
60
60
|
GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
|
|
61
61
|
GameSentenceMiner/web/gsm_websocket.py,sha256=IwwQo6VtgPqeOuc-datgfJyLpX3LwB2MISDqA6EkiSA,4131
|
|
62
62
|
GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
|
|
63
|
-
GameSentenceMiner/web/stats.py,sha256=
|
|
64
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
|
63
|
+
GameSentenceMiner/web/stats.py,sha256=h4tRA_00OdQ9uVYrQuAE5SmeSGHMLfTUHPOy6re_h5s,22366
|
|
64
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=zQx9c8AQ4pWN8JgxvW0MKWkvtoIyrhXBlGdT-acBupU,13484
|
|
65
65
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
66
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
|
67
67
|
GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
|
|
@@ -80,19 +80,19 @@ GameSentenceMiner/web/static/js/database.js,sha256=-SjMmhXzU8a3QNGrwGtJCu55ZXXfk
|
|
|
80
80
|
GameSentenceMiner/web/static/js/kanji-grid.js,sha256=rUa8_TGFm4Z8CtURoAlZjCN032PLe0YmHvN52S4_sE0,7181
|
|
81
81
|
GameSentenceMiner/web/static/js/search.js,sha256=QrjsVXf90c1LzD09TPaK93RbIN9yEiXF8IKAKrDY4hw,10482
|
|
82
82
|
GameSentenceMiner/web/static/js/shared.js,sha256=UL7wDJ5FsqeIspkCcC4s_jOv6a93I2gxFi57gQT8Bn0,21001
|
|
83
|
-
GameSentenceMiner/web/static/js/stats.js,sha256=
|
|
83
|
+
GameSentenceMiner/web/static/js/stats.js,sha256=uAh_J0JngC1QjB_Y9YxRKfWrxfPsdyy6Ypk5WFerZKU,86149
|
|
84
84
|
GameSentenceMiner/web/templates/anki_stats.html,sha256=FdIMl-kY0-3as9Wn0i-wKIIkl1xXn4nWMbWPypN_1T0,10955
|
|
85
85
|
GameSentenceMiner/web/templates/database.html,sha256=Gd54rgZmfcV7ufoJ69COeMncs5Q5u-rSJcsIvROVCEo,13732
|
|
86
86
|
GameSentenceMiner/web/templates/index.html,sha256=7ChQ1j602MOiYU95wXAKP_Ezsh_JgdlGz2uXIzS2j0g,227894
|
|
87
87
|
GameSentenceMiner/web/templates/search.html,sha256=nao3M_hAbm5ftzThi91NtQ3ZiINMPUNx4ngFmqLnLQ4,4060
|
|
88
|
-
GameSentenceMiner/web/templates/stats.html,sha256=
|
|
88
|
+
GameSentenceMiner/web/templates/stats.html,sha256=G3S2YBZ2sQ-D4wTQQBe8411cVTE9QPQq-wr4kyDPi_w,32508
|
|
89
89
|
GameSentenceMiner/web/templates/utility.html,sha256=KtqnZUMAYs5XsEdC9Tlsd40NKAVic0mu6sh-ReMDJpU,16940
|
|
90
90
|
GameSentenceMiner/web/templates/components/navigation.html,sha256=6y9PvM3nh8LY6JWrZb6zVOm0vqkBLDc6d3gB9X5lT_w,1055
|
|
91
91
|
GameSentenceMiner/web/templates/components/theme-styles.html,sha256=hiq3zdJljpRjQO1iUA7gfFKwXebltG-IWW-gnKS4GHA,3439
|
|
92
92
|
GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
93
|
-
gamesentenceminer-2.16.
|
|
94
|
-
gamesentenceminer-2.16.
|
|
95
|
-
gamesentenceminer-2.16.
|
|
96
|
-
gamesentenceminer-2.16.
|
|
97
|
-
gamesentenceminer-2.16.
|
|
98
|
-
gamesentenceminer-2.16.
|
|
93
|
+
gamesentenceminer-2.16.13.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
94
|
+
gamesentenceminer-2.16.13.dist-info/METADATA,sha256=Y5s0ewCxQEPwTEo4MTRyUqaDrfWOiiVfL1qtWDmo-ew,7349
|
|
95
|
+
gamesentenceminer-2.16.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
96
|
+
gamesentenceminer-2.16.13.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
|
97
|
+
gamesentenceminer-2.16.13.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
|
98
|
+
gamesentenceminer-2.16.13.dist-info/RECORD,,
|
|
File without changes
|
{gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{gamesentenceminer-2.16.11.dist-info → gamesentenceminer-2.16.13.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|