GameSentenceMiner 2.16.2__py3-none-any.whl → 2.16.4__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 +15 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/util/configuration.py +5 -0
- GameSentenceMiner/util/db.py +36 -1
- GameSentenceMiner/util/get_overlay_coords.py +59 -2
- GameSentenceMiner/vad.py +2 -2
- GameSentenceMiner/web/database_api.py +170 -5
- GameSentenceMiner/web/static/js/stats.js +141 -2
- GameSentenceMiner/web/templates/stats.html +33 -0
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/RECORD +17 -17
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.16.2.dist-info → gamesentenceminer-2.16.4.dist-info}/top_level.txt +0 -0
GameSentenceMiner/config_gui.py
CHANGED
@@ -391,9 +391,10 @@ class ConfigApp:
|
|
391
391
|
self.use_canned_context_prompt_value = tk.BooleanVar(value=self.settings.ai.use_canned_context_prompt)
|
392
392
|
self.ai_dialogue_context_length_value = tk.StringVar(value=str(self.settings.ai.dialogue_context_length))
|
393
393
|
|
394
|
-
#
|
394
|
+
# Overlay Settings
|
395
395
|
self.overlay_websocket_port_value = tk.StringVar(value=str(self.settings.overlay.websocket_port))
|
396
396
|
self.overlay_websocket_send_value = tk.BooleanVar(value=self.settings.overlay.monitor_to_capture)
|
397
|
+
self.overlay_engine_value = tk.StringVar(value=self.settings.overlay.engine)
|
397
398
|
|
398
399
|
# Master Config Settings
|
399
400
|
self.switch_to_default_if_not_found_value = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
|
@@ -623,7 +624,8 @@ class ConfigApp:
|
|
623
624
|
),
|
624
625
|
overlay=Overlay(
|
625
626
|
websocket_port=int(self.overlay_websocket_port_value.get()),
|
626
|
-
monitor_to_capture=self.overlay_monitor.current() if self.monitors else 0
|
627
|
+
monitor_to_capture=self.overlay_monitor.current() if self.monitors else 0,
|
628
|
+
engine=OverlayEngine(self.overlay_engine_value.get()).value if self.overlay_engine_value.get() else OverlayEngine.LENS.value
|
627
629
|
)
|
628
630
|
# wip=WIP(
|
629
631
|
# overlay_websocket_port=int(self.overlay_websocket_port_value.get()),
|
@@ -2221,7 +2223,17 @@ class ConfigApp:
|
|
2221
2223
|
self.overlay_monitor.current(0)
|
2222
2224
|
self.overlay_monitor.config(state="disabled")
|
2223
2225
|
self.current_row += 1
|
2224
|
-
|
2226
|
+
|
2227
|
+
# Overlay Engine Selection
|
2228
|
+
overlay_engine_i18n = overlay_i18n.get('overlay_engine', {})
|
2229
|
+
HoverInfoLabelWidget(overlay_frame, text=overlay_engine_i18n.get('label', '...'),
|
2230
|
+
tooltip=overlay_engine_i18n.get('tooltip', '...'),
|
2231
|
+
row=self.current_row, column=0)
|
2232
|
+
self.overlay_engine = ttk.Combobox(overlay_frame, values=[e.value for e in OverlayEngine], state="readonly",
|
2233
|
+
textvariable=self.overlay_engine_value)
|
2234
|
+
self.overlay_engine.grid(row=self.current_row, column=1, sticky='EW', pady=2)
|
2235
|
+
self.current_row += 1
|
2236
|
+
|
2225
2237
|
if self.monitors:
|
2226
2238
|
# Ensure the index is valid
|
2227
2239
|
monitor_index = self.settings.overlay.monitor_to_capture
|
@@ -568,6 +568,10 @@
|
|
568
568
|
"label": "Monitor to Capture:",
|
569
569
|
"tooltip": "Select the monitor to capture (1-based index).",
|
570
570
|
"not_detected": "OwOCR Not Detected"
|
571
|
+
},
|
572
|
+
"overlay_engine": {
|
573
|
+
"label": "Overlay Engine:",
|
574
|
+
"tooltip": "Select the OCR engine for the overlay. If you use lens, and are on windows, it will use OneOCR to optimize the scan."
|
571
575
|
}
|
572
576
|
},
|
573
577
|
"wip": {
|
@@ -567,6 +567,10 @@
|
|
567
567
|
"label": "キャプチャ対象モニター:",
|
568
568
|
"tooltip": "キャプチャするモニターを選択(1から始まるインデックス)。",
|
569
569
|
"not_detected": "OwOCRが検出されません"
|
570
|
+
},
|
571
|
+
"overlay_engine": {
|
572
|
+
"label": "オーバーレイエンジン:",
|
573
|
+
"tooltip": "オーバーレイのOCRエンジンを選択します。Lensを使用していてWindowsの場合、スキャンを最適化するためにOneOCRを使用します。"
|
570
574
|
}
|
571
575
|
},
|
572
576
|
"wip": {
|
@@ -556,6 +556,10 @@
|
|
556
556
|
"label": "捕获的显示器:",
|
557
557
|
"tooltip": "选择要捕获的显示器(从1开始的索引)。",
|
558
558
|
"not_detected": "未检测到 OwOCR"
|
559
|
+
},
|
560
|
+
"overlay_engine": {
|
561
|
+
"label": "覆盖层引擎:",
|
562
|
+
"tooltip": "为覆盖层选择 OCR 引擎。如果您使用的是 lens,并且在 windows 上,它将使用 OneOCR 来优化扫描。"
|
559
563
|
}
|
560
564
|
},
|
561
565
|
"wip": {
|
@@ -630,10 +630,15 @@ class Ai:
|
|
630
630
|
self.gemini_model = 'gemini-2.5-flash-lite'
|
631
631
|
|
632
632
|
|
633
|
+
class OverlayEngine(str, Enum):
|
634
|
+
LENS = 'lens'
|
635
|
+
ONEOCR = 'oneocr'
|
636
|
+
|
633
637
|
@dataclass_json
|
634
638
|
@dataclass
|
635
639
|
class Overlay:
|
636
640
|
websocket_port: int = 55499
|
641
|
+
engine: str = OverlayEngine.LENS.value
|
637
642
|
monitor_to_capture: int = 0
|
638
643
|
|
639
644
|
def __post_init__(self):
|
GameSentenceMiner/util/db.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
5
|
+
import shutil
|
5
6
|
import sqlite3
|
6
7
|
from sys import platform
|
7
8
|
import time
|
@@ -10,6 +11,7 @@ import threading
|
|
10
11
|
|
11
12
|
from GameSentenceMiner.util.text_log import GameLine
|
12
13
|
from GameSentenceMiner.util.configuration import logger, is_dev
|
14
|
+
import gzip
|
13
15
|
|
14
16
|
|
15
17
|
class SQLiteDB:
|
@@ -399,7 +401,40 @@ def get_db_directory():
|
|
399
401
|
return os.path.join(config_dir, 'gsm.db')
|
400
402
|
|
401
403
|
|
402
|
-
|
404
|
+
# Backup and compress the database on load, with today's date, up to 5 days ago (clean up old backups)
|
405
|
+
def backup_db(db_path: str):
|
406
|
+
backup_dir = os.path.join(os.path.dirname(db_path), "backup", "database")
|
407
|
+
os.makedirs(backup_dir, exist_ok=True)
|
408
|
+
today = time.strftime("%Y-%m-%d")
|
409
|
+
backup_file = os.path.join(backup_dir, f"gsm_{today}.db.gz")
|
410
|
+
|
411
|
+
# Test, remove backups older than 60 minutes
|
412
|
+
# cutoff = time.time() - 60 * 60
|
413
|
+
# Clean up backups older than 5 days
|
414
|
+
cutoff = time.time() - 5 * 24 * 60 * 60
|
415
|
+
for fname in os.listdir(backup_dir):
|
416
|
+
fpath = os.path.join(backup_dir, fname)
|
417
|
+
if fname.startswith("gsm_") and fname.endswith(".db.gz"):
|
418
|
+
try:
|
419
|
+
file_time = os.path.getmtime(fpath)
|
420
|
+
if file_time < cutoff:
|
421
|
+
os.remove(fpath)
|
422
|
+
logger.info(f"Old backup removed: {fpath}")
|
423
|
+
except Exception as e:
|
424
|
+
logger.warning(f"Failed to remove old backup {fpath}: {e}")
|
425
|
+
|
426
|
+
# Create backup if not already present for today
|
427
|
+
if not os.path.exists(backup_file):
|
428
|
+
with open(db_path, "rb") as f_in, open(backup_file, "wb") as f_out:
|
429
|
+
with gzip.GzipFile(fileobj=f_out, mode="wb") as gz_out:
|
430
|
+
shutil.copyfileobj(f_in, gz_out)
|
431
|
+
logger.info(f"Database backup created: {backup_file}")
|
432
|
+
|
433
|
+
db_path = get_db_directory()
|
434
|
+
if os.path.exists(db_path):
|
435
|
+
backup_db(db_path)
|
436
|
+
|
437
|
+
gsm_db = SQLiteDB(db_path)
|
403
438
|
|
404
439
|
for cls in [AIModelsTable, GameLinesTable]:
|
405
440
|
cls.set_db(gsm_db)
|
@@ -1,19 +1,67 @@
|
|
1
1
|
import asyncio
|
2
2
|
import io
|
3
3
|
import base64
|
4
|
+
import json
|
4
5
|
import math
|
5
6
|
import os
|
6
7
|
import time
|
7
8
|
from PIL import Image
|
8
9
|
from typing import Dict, Any, List, Tuple
|
10
|
+
import json
|
11
|
+
from rapidfuzz.distance import Levenshtein
|
9
12
|
|
10
13
|
# Local application imports
|
11
14
|
from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness
|
12
|
-
from GameSentenceMiner.util.configuration import get_config, is_windows
|
15
|
+
from GameSentenceMiner.util.configuration import OverlayEngine, get_config, is_windows, is_beangate, logger
|
13
16
|
from GameSentenceMiner.util.electron_config import get_ocr_language
|
14
|
-
from GameSentenceMiner.obs import get_screenshot_PIL
|
17
|
+
from GameSentenceMiner.obs import get_screenshot_PIL
|
15
18
|
from GameSentenceMiner.web.texthooking_page import send_word_coordinates_to_overlay
|
16
19
|
|
20
|
+
# def align_and_correct(ocr_json, reference_text):
|
21
|
+
# logger.info(f"Starting align_and_correct with reference_text: '{reference_text}'")
|
22
|
+
# corrected = []
|
23
|
+
# ref_chars = list(reference_text)
|
24
|
+
# logger.info(f"Reference chars: {ref_chars}")
|
25
|
+
|
26
|
+
# for block_idx, block in enumerate(ocr_json):
|
27
|
+
# logger.info(f"Processing block {block_idx}: {block}")
|
28
|
+
# ocr_chars = [w["text"] for w in block["words"]]
|
29
|
+
# ocr_str = "".join(ocr_chars)
|
30
|
+
|
31
|
+
# # Compute edit operations from OCR → Reference
|
32
|
+
# ops = Levenshtein.editops(ocr_str, "".join(ref_chars))
|
33
|
+
|
34
|
+
# corrected_words = block["words"][:]
|
35
|
+
|
36
|
+
# # Apply corrections
|
37
|
+
# for op_idx, (op, i, j) in enumerate(ops):
|
38
|
+
# logger.info(f"Operation {op_idx}: {op}, i={i}, j={j}")
|
39
|
+
# if op == "replace":
|
40
|
+
# logger.info(f"Replacing word at index {i} ('{corrected_words[i]['text']}') with reference char '{ref_chars[j]}'")
|
41
|
+
# corrected_words[i]["text"] = ref_chars[j]
|
42
|
+
# elif op == "insert":
|
43
|
+
# if i > 0:
|
44
|
+
# prev = corrected_words[i - 1]["bounding_rect"]
|
45
|
+
# bbox = prev # simple: copy neighbor bbox
|
46
|
+
# else:
|
47
|
+
# bbox = corrected_words[0]["bounding_rect"]
|
48
|
+
# corrected_words.insert(i, {
|
49
|
+
# "text": ref_chars[j],
|
50
|
+
# "bounding_rect": bbox,
|
51
|
+
# "confidence": 1.0
|
52
|
+
# })
|
53
|
+
# elif op == "delete":
|
54
|
+
# logger.info(f"Deleting word at index {i} ('{corrected_words[i]['text']}')")
|
55
|
+
# corrected_words[i]["text"] = "" # mark empty
|
56
|
+
|
57
|
+
# corrected_words = [w for w in corrected_words if w["text"]]
|
58
|
+
|
59
|
+
# block["words"] = corrected_words
|
60
|
+
# block["text"] = "".join(w["text"] for w in corrected_words)
|
61
|
+
# corrected.append(block)
|
62
|
+
|
63
|
+
# return corrected
|
64
|
+
|
17
65
|
# Conditionally import OCR engines
|
18
66
|
try:
|
19
67
|
if os.path.exists(os.path.expanduser('~/.config/oneocr/oneocr.dll')):
|
@@ -218,6 +266,15 @@ class OverlayProcessor:
|
|
218
266
|
)
|
219
267
|
else:
|
220
268
|
composite_image = full_screenshot
|
269
|
+
|
270
|
+
# If User Home is beangate
|
271
|
+
if is_beangate:
|
272
|
+
with open("oneocr_results.json", "w", encoding="utf-8") as f:
|
273
|
+
f.write(json.dumps(oneocr_results, ensure_ascii=False, indent=2))
|
274
|
+
|
275
|
+
if get_config().overlay.engine == OverlayEngine.ONEOCR.value and self.oneocr:
|
276
|
+
logger.info("Using OneOCR results for overlay as configured.")
|
277
|
+
return oneocr_results
|
221
278
|
|
222
279
|
# 4. Use Google Lens on the cleaner composite image for higher accuracy
|
223
280
|
res = self.lens(
|
GameSentenceMiner/vad.py
CHANGED
@@ -202,7 +202,7 @@ class WhisperVADProcessor(VADProcessor):
|
|
202
202
|
logger.info(
|
203
203
|
"Unknown single character segment, not skipping, but logging, please report if this is a mistake: " + segment.text)
|
204
204
|
|
205
|
-
if segment.no_speech_prob and segment.no_speech_prob > 0.
|
205
|
+
if segment.no_speech_prob and segment.no_speech_prob > 0.9:
|
206
206
|
logger.debug(f"Skipping segment with high no_speech_prob: {segment.no_speech_prob} for segment {segment.text} at {segment.start}-{segment.end}")
|
207
207
|
continue
|
208
208
|
|
@@ -211,7 +211,7 @@ class WhisperVADProcessor(VADProcessor):
|
|
211
211
|
logger.debug(f"Skipping segment with low unique words: {unique_words} for segment {segment.text} at {segment.start}-{segment.end}")
|
212
212
|
continue
|
213
213
|
|
214
|
-
if
|
214
|
+
if segment.seek > 0 and segment.no_speech_prob > .3:
|
215
215
|
logger.debug(f"Skipping segment after long pause with high no_speech_prob after: {segment.no_speech_prob} for segment {segment.text} at {segment.start}-{segment.end}")
|
216
216
|
continue
|
217
217
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import datetime
|
2
2
|
import re
|
3
|
+
import csv
|
4
|
+
import io
|
3
5
|
from collections import defaultdict
|
4
6
|
|
5
7
|
import flask
|
@@ -135,8 +137,8 @@ def register_database_api_routes(app):
|
|
135
137
|
'date_range': f"{min_date.strftime('%Y-%m-%d')} to {max_date.strftime('%Y-%m-%d')}" if min_date != max_date else min_date.strftime('%Y-%m-%d')
|
136
138
|
})
|
137
139
|
|
138
|
-
# Sort by
|
139
|
-
games_data.sort(key=lambda x: x['
|
140
|
+
# Sort by total characters (most characters first)
|
141
|
+
games_data.sort(key=lambda x: x['total_characters'], reverse=True)
|
140
142
|
|
141
143
|
return jsonify({'games': games_data}), 200
|
142
144
|
|
@@ -732,7 +734,8 @@ def register_database_api_routes(app):
|
|
732
734
|
color = colors[i % len(colors)]
|
733
735
|
|
734
736
|
datasets.append({
|
735
|
-
"label": f"{game}
|
737
|
+
"label": f"{game}",
|
738
|
+
"for": "Lines Received",
|
736
739
|
"data": final_data[game]['lines'],
|
737
740
|
"borderColor": color,
|
738
741
|
"backgroundColor": f"{color}33", # Semi-transparent for fill
|
@@ -740,7 +743,8 @@ def register_database_api_routes(app):
|
|
740
743
|
"tension": 0.1
|
741
744
|
})
|
742
745
|
datasets.append({
|
743
|
-
"label": f"{game}
|
746
|
+
"label": f"{game}",
|
747
|
+
"for": "Characters Read",
|
744
748
|
"data": final_data[game]['chars'],
|
745
749
|
"borderColor": color,
|
746
750
|
"backgroundColor": f"{color}33",
|
@@ -780,4 +784,165 @@ def register_database_api_routes(app):
|
|
780
784
|
"currentGameStats": current_game_stats,
|
781
785
|
"allGamesStats": all_games_stats,
|
782
786
|
"allLinesData": all_lines_data
|
783
|
-
})
|
787
|
+
})
|
788
|
+
|
789
|
+
@app.route('/api/import-exstatic', methods=['POST'])
|
790
|
+
def api_import_exstatic():
|
791
|
+
"""
|
792
|
+
Import ExStatic CSV data into GSM database.
|
793
|
+
Expected CSV format: uuid,given_identifier,name,line,time
|
794
|
+
"""
|
795
|
+
try:
|
796
|
+
# Check if file is provided
|
797
|
+
if 'file' not in request.files:
|
798
|
+
return jsonify({'error': 'No file provided'}), 400
|
799
|
+
|
800
|
+
file = request.files['file']
|
801
|
+
if file.filename == '':
|
802
|
+
return jsonify({'error': 'No file selected'}), 400
|
803
|
+
|
804
|
+
# Validate file type
|
805
|
+
if not file.filename.lower().endswith('.csv'):
|
806
|
+
return jsonify({'error': 'File must be a CSV file'}), 400
|
807
|
+
|
808
|
+
# Read and parse CSV
|
809
|
+
try:
|
810
|
+
# Read file content as text with proper encoding handling
|
811
|
+
file_content = file.read().decode('utf-8-sig') # Handle BOM if present
|
812
|
+
|
813
|
+
# First, get the header line manually to avoid issues with multi-line content
|
814
|
+
lines = file_content.split('\n')
|
815
|
+
if len(lines) == 1 and not lines[0].strip():
|
816
|
+
return jsonify({'error': 'Empty CSV file'}), 400
|
817
|
+
|
818
|
+
header_line = lines[0].strip()
|
819
|
+
logger.info(f"Header line: {header_line}")
|
820
|
+
|
821
|
+
# Parse headers manually
|
822
|
+
header_reader = csv.reader([header_line])
|
823
|
+
try:
|
824
|
+
headers = next(header_reader)
|
825
|
+
headers = [h.strip() for h in headers] # Clean whitespace
|
826
|
+
logger.info(f"Parsed headers: {headers}")
|
827
|
+
except StopIteration:
|
828
|
+
return jsonify({'error': 'Could not parse CSV headers'}), 400
|
829
|
+
|
830
|
+
# Validate headers
|
831
|
+
expected_headers = {'uuid', 'given_identifier', 'name', 'line', 'time'}
|
832
|
+
actual_headers = set(headers)
|
833
|
+
|
834
|
+
if not expected_headers.issubset(actual_headers):
|
835
|
+
missing_headers = expected_headers - actual_headers
|
836
|
+
# Check if this looks like a stats CSV instead of lines CSV
|
837
|
+
if 'client' in actual_headers and 'chars_read' in actual_headers:
|
838
|
+
return jsonify({
|
839
|
+
'error': 'This appears to be an ExStatic stats CSV. Please upload the ExStatic lines CSV file instead. The lines CSV should contain columns: uuid, given_identifier, name, line, time'
|
840
|
+
}), 400
|
841
|
+
else:
|
842
|
+
return jsonify({
|
843
|
+
'error': f'Invalid CSV format. Missing required columns: {", ".join(missing_headers)}. Expected format: uuid, given_identifier, name, line, time. Found headers: {", ".join(actual_headers)}'
|
844
|
+
}), 400
|
845
|
+
|
846
|
+
# Now parse the full CSV with proper handling for multi-line fields
|
847
|
+
file_io = io.StringIO(file_content)
|
848
|
+
csv_reader = csv.DictReader(file_io, quoting=csv.QUOTE_MINIMAL, skipinitialspace=True)
|
849
|
+
|
850
|
+
# Process CSV rows
|
851
|
+
imported_lines = []
|
852
|
+
games_set = set()
|
853
|
+
errors = []
|
854
|
+
seen_uuids = set() # Track UUIDs within this import batch
|
855
|
+
|
856
|
+
for row_num, row in enumerate(csv_reader):
|
857
|
+
try:
|
858
|
+
# Extract and validate required fields
|
859
|
+
uuid = row.get('uuid', '').strip()
|
860
|
+
name = row.get('name', '').strip()
|
861
|
+
line = row.get('line', '').strip()
|
862
|
+
time_str = row.get('time', '').strip()
|
863
|
+
|
864
|
+
# Validate required fields
|
865
|
+
if not uuid:
|
866
|
+
errors.append(f"Row {row_num}: Missing UUID")
|
867
|
+
continue
|
868
|
+
if not name:
|
869
|
+
errors.append(f"Row {row_num}: Missing name")
|
870
|
+
continue
|
871
|
+
if not line:
|
872
|
+
errors.append(f"Row {row_num}: Missing line text")
|
873
|
+
continue
|
874
|
+
if not time_str:
|
875
|
+
errors.append(f"Row {row_num}: Missing time")
|
876
|
+
continue
|
877
|
+
|
878
|
+
# Check for duplicates within this import batch
|
879
|
+
if uuid in seen_uuids:
|
880
|
+
logger.info(f"Skipping duplicate UUID within import batch: {uuid}")
|
881
|
+
continue
|
882
|
+
seen_uuids.add(uuid)
|
883
|
+
|
884
|
+
# Convert time to timestamp
|
885
|
+
try:
|
886
|
+
timestamp = float(time_str)
|
887
|
+
except ValueError:
|
888
|
+
errors.append(f"Row {row_num}: Invalid time format: {time_str}")
|
889
|
+
continue
|
890
|
+
|
891
|
+
# Clean up line text (remove extra whitespace and newlines)
|
892
|
+
line_text = line.strip()
|
893
|
+
|
894
|
+
# Check if this UUID already exists in database
|
895
|
+
existing_line = GameLinesTable.get(uuid)
|
896
|
+
if existing_line:
|
897
|
+
logger.info(f"Skipping duplicate UUID already in database: {uuid}")
|
898
|
+
continue
|
899
|
+
|
900
|
+
# Create GameLinesTable entry
|
901
|
+
game_line = GameLinesTable(
|
902
|
+
id=uuid,
|
903
|
+
game_name=name,
|
904
|
+
line_text=line_text,
|
905
|
+
timestamp=timestamp
|
906
|
+
)
|
907
|
+
|
908
|
+
imported_lines.append(game_line)
|
909
|
+
games_set.add(name)
|
910
|
+
|
911
|
+
except Exception as e:
|
912
|
+
errors.append(f"Row {row_num}: Error processing row - {str(e)}")
|
913
|
+
continue
|
914
|
+
|
915
|
+
# Import lines into database
|
916
|
+
imported_count = 0
|
917
|
+
for game_line in imported_lines:
|
918
|
+
try:
|
919
|
+
game_line.add()
|
920
|
+
imported_count += 1
|
921
|
+
except Exception as e:
|
922
|
+
logger.error(f"Failed to import line {game_line.id}: {e}")
|
923
|
+
errors.append(f"Failed to import line {game_line.id}: {str(e)}")
|
924
|
+
|
925
|
+
# Prepare response
|
926
|
+
response_data = {
|
927
|
+
'message': f'Successfully imported {imported_count} lines from {len(games_set)} games',
|
928
|
+
'imported_count': imported_count,
|
929
|
+
'games_count': len(games_set),
|
930
|
+
'games': list(games_set)
|
931
|
+
}
|
932
|
+
|
933
|
+
if errors:
|
934
|
+
response_data['warnings'] = errors
|
935
|
+
response_data['warning_count'] = len(errors)
|
936
|
+
|
937
|
+
logger.info(f"ExStatic import completed: {imported_count} lines from {len(games_set)} games")
|
938
|
+
|
939
|
+
return jsonify(response_data), 200
|
940
|
+
|
941
|
+
except csv.Error as e:
|
942
|
+
return jsonify({'error': f'CSV parsing error: {str(e)}'}), 400
|
943
|
+
except UnicodeDecodeError:
|
944
|
+
return jsonify({'error': 'File encoding error. Please ensure the CSV is UTF-8 encoded.'}), 400
|
945
|
+
|
946
|
+
except Exception as e:
|
947
|
+
logger.error(f"Error in ExStatic import: {e}")
|
948
|
+
return jsonify({'error': f'Import failed: {str(e)}'}), 500
|
@@ -725,12 +725,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
725
725
|
// Filter datasets for each chart
|
726
726
|
const linesData = {
|
727
727
|
labels: data.labels,
|
728
|
-
datasets: data.datasets.filter(d => d.
|
728
|
+
datasets: data.datasets.filter(d => d.for === "Lines Received")
|
729
729
|
};
|
730
730
|
|
731
731
|
const charsData = {
|
732
732
|
labels: data.labels,
|
733
|
-
datasets: data.datasets.filter(d => d.
|
733
|
+
datasets: data.datasets.filter(d => d.for === 'Characters Read')
|
734
734
|
};
|
735
735
|
|
736
736
|
// Remove the 'hidden' property so they appear on their own charts
|
@@ -1424,4 +1424,143 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
1424
1424
|
if (document.getElementById('gamesTableBody')) {
|
1425
1425
|
new GameDeletionManager();
|
1426
1426
|
}
|
1427
|
+
|
1428
|
+
// ExStatic Import Functionality
|
1429
|
+
const exstaticFileInput = document.getElementById('exstaticFile');
|
1430
|
+
const importExstaticBtn = document.getElementById('importExstaticBtn');
|
1431
|
+
const importProgress = document.getElementById('importProgress');
|
1432
|
+
const importProgressBar = document.getElementById('importProgressBar');
|
1433
|
+
const importProgressText = document.getElementById('importProgressText');
|
1434
|
+
const importStatus = document.getElementById('importStatus');
|
1435
|
+
|
1436
|
+
if (exstaticFileInput && importExstaticBtn) {
|
1437
|
+
// Enable/disable import button based on file selection
|
1438
|
+
exstaticFileInput.addEventListener('change', function(e) {
|
1439
|
+
const file = e.target.files[0];
|
1440
|
+
if (file && file.type === 'text/csv' && file.name.toLowerCase().endsWith('.csv')) {
|
1441
|
+
importExstaticBtn.disabled = false;
|
1442
|
+
importExstaticBtn.style.background = '#2980b9';
|
1443
|
+
importExstaticBtn.style.cursor = 'pointer';
|
1444
|
+
showImportStatus('', 'info', false);
|
1445
|
+
} else {
|
1446
|
+
importExstaticBtn.disabled = true;
|
1447
|
+
importExstaticBtn.style.background = '#666';
|
1448
|
+
importExstaticBtn.style.cursor = 'not-allowed';
|
1449
|
+
if (file) {
|
1450
|
+
showImportStatus('Please select a valid CSV file.', 'error', true);
|
1451
|
+
}
|
1452
|
+
}
|
1453
|
+
});
|
1454
|
+
|
1455
|
+
// Handle import button click
|
1456
|
+
importExstaticBtn.addEventListener('click', function() {
|
1457
|
+
const file = exstaticFileInput.files[0];
|
1458
|
+
if (!file) {
|
1459
|
+
showImportStatus('Please select a CSV file first.', 'error', true);
|
1460
|
+
return;
|
1461
|
+
}
|
1462
|
+
|
1463
|
+
importExstaticData(file);
|
1464
|
+
});
|
1465
|
+
}
|
1466
|
+
|
1467
|
+
function showImportStatus(message, type, show) {
|
1468
|
+
if (!importStatus) return;
|
1469
|
+
|
1470
|
+
if (show && message) {
|
1471
|
+
importStatus.textContent = message;
|
1472
|
+
importStatus.style.display = 'block';
|
1473
|
+
|
1474
|
+
// Set appropriate styling based on type
|
1475
|
+
if (type === 'error') {
|
1476
|
+
importStatus.style.background = 'var(--danger-color)';
|
1477
|
+
importStatus.style.color = 'white';
|
1478
|
+
} else if (type === 'success') {
|
1479
|
+
importStatus.style.background = 'var(--success-color)';
|
1480
|
+
importStatus.style.color = 'white';
|
1481
|
+
} else if (type === 'info') {
|
1482
|
+
importStatus.style.background = 'var(--primary-color)';
|
1483
|
+
importStatus.style.color = 'white';
|
1484
|
+
} else {
|
1485
|
+
importStatus.style.background = 'var(--bg-tertiary)';
|
1486
|
+
importStatus.style.color = 'var(--text-primary)';
|
1487
|
+
}
|
1488
|
+
} else {
|
1489
|
+
importStatus.style.display = 'none';
|
1490
|
+
}
|
1491
|
+
}
|
1492
|
+
|
1493
|
+
function showImportProgress(show, percentage = 0) {
|
1494
|
+
if (!importProgress || !importProgressBar || !importProgressText) return;
|
1495
|
+
|
1496
|
+
if (show) {
|
1497
|
+
importProgress.style.display = 'block';
|
1498
|
+
importProgressBar.style.width = percentage + '%';
|
1499
|
+
importProgressText.textContent = Math.round(percentage) + '%';
|
1500
|
+
} else {
|
1501
|
+
importProgress.style.display = 'none';
|
1502
|
+
}
|
1503
|
+
}
|
1504
|
+
|
1505
|
+
async function importExstaticData(file) {
|
1506
|
+
try {
|
1507
|
+
// Disable import button and show progress
|
1508
|
+
importExstaticBtn.disabled = true;
|
1509
|
+
showImportProgress(true, 0);
|
1510
|
+
showImportStatus('Preparing import...', 'info', true);
|
1511
|
+
|
1512
|
+
// Create FormData and append the file
|
1513
|
+
const formData = new FormData();
|
1514
|
+
formData.append('file', file);
|
1515
|
+
|
1516
|
+
// Show upload progress
|
1517
|
+
showImportProgress(true, 25);
|
1518
|
+
showImportStatus('Uploading file...', 'info', true);
|
1519
|
+
|
1520
|
+
// Send file to backend
|
1521
|
+
const response = await fetch('/api/import-exstatic', {
|
1522
|
+
method: 'POST',
|
1523
|
+
body: formData
|
1524
|
+
});
|
1525
|
+
|
1526
|
+
showImportProgress(true, 75);
|
1527
|
+
showImportStatus('Processing data...', 'info', true);
|
1528
|
+
|
1529
|
+
const result = await response.json();
|
1530
|
+
|
1531
|
+
showImportProgress(true, 100);
|
1532
|
+
|
1533
|
+
if (response.ok) {
|
1534
|
+
// Success
|
1535
|
+
const message = `Successfully imported ${result.imported_count || 0} lines from ${result.games_count || 0} games.`;
|
1536
|
+
showImportStatus(message, 'success', true);
|
1537
|
+
|
1538
|
+
// Reset file input and button
|
1539
|
+
exstaticFileInput.value = '';
|
1540
|
+
importExstaticBtn.disabled = true;
|
1541
|
+
|
1542
|
+
// Hide progress after a delay
|
1543
|
+
setTimeout(() => {
|
1544
|
+
showImportProgress(false);
|
1545
|
+
// Optionally refresh the page to show new data
|
1546
|
+
if (result.imported_count > 0) {
|
1547
|
+
setTimeout(() => {
|
1548
|
+
window.location.reload();
|
1549
|
+
}, 2000);
|
1550
|
+
}
|
1551
|
+
}, 1500);
|
1552
|
+
} else {
|
1553
|
+
// Error
|
1554
|
+
showImportStatus(result.error || 'Import failed. Please try again.', 'error', true);
|
1555
|
+
showImportProgress(false);
|
1556
|
+
}
|
1557
|
+
} catch (error) {
|
1558
|
+
console.error('Import error:', error);
|
1559
|
+
showImportStatus('Import failed due to network error. Please try again.', 'error', true);
|
1560
|
+
showImportProgress(false);
|
1561
|
+
} finally {
|
1562
|
+
// Re-enable import button only if a file is still selected
|
1563
|
+
importExstaticBtn.disabled = !(exstaticFileInput && exstaticFileInput.files && exstaticFileInput.files.length > 0);
|
1564
|
+
}
|
1565
|
+
}
|
1427
1566
|
});
|
@@ -313,6 +313,39 @@
|
|
313
313
|
Minimum hours of reading activity required to maintain streak (0.01-24 hours)
|
314
314
|
</small>
|
315
315
|
</div>
|
316
|
+
|
317
|
+
<!-- Import ExStatic Lines Section -->
|
318
|
+
<div style="margin-bottom: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
|
319
|
+
<label style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">
|
320
|
+
Import ExStatic Lines
|
321
|
+
</label>
|
322
|
+
<div style="margin-bottom: 10px;">
|
323
|
+
<input
|
324
|
+
type="file"
|
325
|
+
id="exstaticFile"
|
326
|
+
accept=".csv"
|
327
|
+
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
|
328
|
+
>
|
329
|
+
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
|
330
|
+
Select an ExStatic CSV file to import reading data into GSM
|
331
|
+
</small>
|
332
|
+
</div>
|
333
|
+
<button
|
334
|
+
type="button"
|
335
|
+
id="importExstaticBtn"
|
336
|
+
style="width: 100%; padding: 10px; background: #666; color: white; border: none; border-radius: 5px; font-size: 14px; cursor: not-allowed; margin-bottom: 10px;"
|
337
|
+
disabled
|
338
|
+
>
|
339
|
+
Import ExStatic Lines
|
340
|
+
</button>
|
341
|
+
<div id="importProgress" style="display: none; margin-bottom: 10px;">
|
342
|
+
<div style="background: var(--bg-tertiary); border-radius: 5px; overflow: hidden; height: 20px; position: relative;">
|
343
|
+
<div id="importProgressBar" style="background: var(--primary-color); height: 100%; width: 0%; transition: width 0.3s ease;"></div>
|
344
|
+
<span id="importProgressText" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; color: var(--text-primary);">0%</span>
|
345
|
+
</div>
|
346
|
+
</div>
|
347
|
+
<div id="importStatus" style="display: none; padding: 10px; border-radius: 5px; font-size: 14px;"></div>
|
348
|
+
</div>
|
316
349
|
</form>
|
317
350
|
|
318
351
|
<div id="settingsError" style="display: none; background: var(--danger-color); color: white; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 14px;"></div>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
GameSentenceMiner/anki.py,sha256=rm9JuRP-1Eba2wcVQ2PZUMB5P9UMEZ99Fh371K0Qfhk,26319
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=xpv2g7LaKOmv98nHTkOtYpsl3dMFMwmHbp5Ioms2v54,141667
|
4
4
|
GameSentenceMiner/gametext.py,sha256=fgBgLchezpauWELE9Y5G3kVCLfAneD0X4lJFoI3FYbs,10351
|
5
5
|
GameSentenceMiner/gsm.py,sha256=CcXGZI8K7cYB9FJfCRaOujlWDk08sQGQXhTiMewMhFg,31889
|
6
6
|
GameSentenceMiner/obs.py,sha256=EyAYhaLvMjoeC-3j7fuvkqZN5logFFanPfb8Wn1C6m0,27296
|
7
|
-
GameSentenceMiner/vad.py,sha256=
|
7
|
+
GameSentenceMiner/vad.py,sha256=ZC1U31C0qHDtWEmk1iYVcNyhrKfKxC1aOZpbP1pv9Oo,20145
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
GameSentenceMiner/ai/ai_prompting.py,sha256=41xdBzE88Jlt12A0D-T_cMfLO5j6MSxfniOptpwNZm0,24068
|
10
10
|
GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -15,9 +15,9 @@ GameSentenceMiner/assets/icon32.png,sha256=Kww0hU_qke9_22wBuO_Nq0Dv2SfnOLwMhCyGg
|
|
15
15
|
GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB_7W4sqs,192487
|
16
16
|
GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
|
17
17
|
GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
|
18
|
-
GameSentenceMiner/locales/en_us.json,sha256=
|
19
|
-
GameSentenceMiner/locales/ja_jp.json,sha256=
|
20
|
-
GameSentenceMiner/locales/zh_cn.json,sha256=
|
18
|
+
GameSentenceMiner/locales/en_us.json,sha256=Wc2sOs5JoWB3_oqA5yVmvslPa60Ud-lpGm5g4Mkp8QE,26969
|
19
|
+
GameSentenceMiner/locales/ja_jp.json,sha256=q923lIhVrofKM9ff1puwagGLvIGoV6LRvD_smoAnZn8,28809
|
20
|
+
GameSentenceMiner/locales/zh_cn.json,sha256=8f94Jy66ox7XCLYIbdbBjLSBgSBA3fV3a1meTxutCOw,25062
|
21
21
|
GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ov04c-nKzh3sADxO-5JyZWVe4DlrHM9edM9tc7-97Jo,5970
|
23
23
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
@@ -37,11 +37,11 @@ GameSentenceMiner/tools/furigana_filter_preview.py,sha256=BXv7FChPEJW_VeG5XYt6su
|
|
37
37
|
GameSentenceMiner/tools/ss_selector.py,sha256=cbjMxiKOCuOfbRvLR_PCRlykBrGtm1LXd6u5czPqkmc,4793
|
38
38
|
GameSentenceMiner/tools/window_transparency.py,sha256=GtbxbmZg0-UYPXhfHff-7IKZyY2DKe4B9GdyovfmpeM,8166
|
39
39
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
-
GameSentenceMiner/util/configuration.py,sha256=
|
41
|
-
GameSentenceMiner/util/db.py,sha256=
|
40
|
+
GameSentenceMiner/util/configuration.py,sha256=XANq-FJ_9tNFQSQvozHnn7fGnRiiKH82_aPHwyUfd8g,40528
|
41
|
+
GameSentenceMiner/util/db.py,sha256=p_oRoj7p_vRlZq1srtZUxpHPlaJcWOhNLC7z9Dclo7A,18052
|
42
42
|
GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
|
43
43
|
GameSentenceMiner/util/ffmpeg.py,sha256=jA-cFtCmdCWrUSPpdtFSLr-GSoqs4qNUzW20v4HPHf0,28715
|
44
|
-
GameSentenceMiner/util/get_overlay_coords.py,sha256=
|
44
|
+
GameSentenceMiner/util/get_overlay_coords.py,sha256=TEMxhrBE8302WQj2K5q7SLKUhgNH_J-RjHpkUTQJdcU,17577
|
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
|
@@ -56,7 +56,7 @@ GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UT
|
|
56
56
|
GameSentenceMiner/util/win10toast/__init__.py,sha256=6TL2w6rzNmpJEp6_v2cAJP_7ExA3UsKzwdM08pNcVfE,5341
|
57
57
|
GameSentenceMiner/util/win10toast/__main__.py,sha256=5MYnBcFj8y_6Dyc1kiPd0_FsUuh4yl1cv5wsleU6V4w,668
|
58
58
|
GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
|
-
GameSentenceMiner/web/database_api.py,sha256=
|
59
|
+
GameSentenceMiner/web/database_api.py,sha256=uSEgneO9Tq7FxmBmXSXi5Y1_Z37YL0jVb97SpK_qFDo,43088
|
60
60
|
GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
|
61
61
|
GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
|
62
62
|
GameSentenceMiner/web/stats.py,sha256=daSSxWlumAyqVVtX10qHESF-tZYwCcFMp8qZA5AE0nI,22066
|
@@ -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=nXvHZelCQQlTWJwaF_j3RnU5v_JFVIDQSEtnauDy8lg,9484
|
82
82
|
GameSentenceMiner/web/static/js/shared.js,sha256=HdQWRToX9U09h0x35K00RrHFw6mBkCPRf7JoLwN0l0c,18410
|
83
|
-
GameSentenceMiner/web/static/js/stats.js,sha256=
|
83
|
+
GameSentenceMiner/web/static/js/stats.js,sha256=EOyM6ZyTQu1IubBQwjnB4mE9vVH7LJwsiUl5Q6LiRCo,66673
|
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=OGfzhNc4QP_kmJdCnEnLm5pzgW6XKERoin0oQZxvg5s,3851
|
88
|
-
GameSentenceMiner/web/templates/stats.html,sha256=
|
88
|
+
GameSentenceMiner/web/templates/stats.html,sha256=sV1nea99Z6oo5ZtVChqAo3dttoU38FaUEpW9SXEv-Bs,19395
|
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.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
94
|
+
gamesentenceminer-2.16.4.dist-info/METADATA,sha256=UZLwXl3tjUMWpOaHIdZAzz7-we6nTUn71tKndiaLscM,7348
|
95
|
+
gamesentenceminer-2.16.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
96
|
+
gamesentenceminer-2.16.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
97
|
+
gamesentenceminer-2.16.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
98
|
+
gamesentenceminer-2.16.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|