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.
@@ -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.distance import Levenshtein
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 # Ensure thread exits when main program exits
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 get_config().overlay.periodic and overlay_server_thread.has_clients():
107
- await self.overlay_processor.find_box_and_send_to_overlay('')
108
- await asyncio.sleep(get_config().overlay.periodic_interval) # Adjust the interval as needed
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
- await asyncio.sleep(3) # Sleep briefly when not active
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
- _, _, oneocr_results, crop_coords_list = self.oneocr(
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
- _, _, coords = res
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 res or not coords:
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 > 0) {
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
- const afkTimerSeconds = 120; // Default AFK timer - should be fetched from settings
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
- reading_hours_target: 1500,
842
- character_count_target: 25000000,
843
- games_target: 100
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
- const afkTimerSeconds = 120; // Default AFK timer
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
- const sessionGapDefault = 3600; // 1 hour in seconds
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; // default
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);
@@ -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
- streak_requirement_hours = getattr(get_config().advanced, 'streak_requirement_hours', 1.0)
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 > get_stats_config().session_gap_seconds:
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 > 3600: # 1 hour 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
- return render_template('stats.html')
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
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.16.11
3
+ Version: 2.16.13
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -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=iOsi3IVati6JHCnoCIzLAxlrbZt4j4pL5KNFzSq4gJo,146121
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=pkG8_3l3fuhm3DzFLuNwSeaYTkRMtyU3O7DB6s372QM,18961
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=VM4PwQRVjuwDcFAz08_7yWC0T7fGAfmVUDuxYuyiswA,22078
64
- GameSentenceMiner/web/texthooking_page.py,sha256=gB2i3X7DQ9kEML6ak4PFSKMi6zd6nPEa_ob-XG4guBo,13231
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=vuLqyrr42u44G2X01vg2tL277Gslz7cNRnpFUB22dNw,83445
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=BL7HQLs62RbPbo5Hs8BgdH9CsrGLH583JSn0Uz3PK5Y,31536
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.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- gamesentenceminer-2.16.11.dist-info/METADATA,sha256=vN1yQWVBqhIYzJ4ygcNNFdok5bC_7_gvtMYn7gKmrwc,7349
95
- gamesentenceminer-2.16.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
- gamesentenceminer-2.16.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
97
- gamesentenceminer-2.16.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
98
- gamesentenceminer-2.16.11.dist-info/RECORD,,
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,,