GameSentenceMiner 2.15.9__py3-none-any.whl → 2.15.11__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.
@@ -1,201 +1,52 @@
1
1
  import asyncio
2
- from ctypes.util import test
3
2
  import datetime
4
3
  import json
5
4
  import os
6
- import queue
7
- import sqlite3
8
5
  import threading
9
- from dataclasses import dataclass
10
6
 
11
7
  import flask
12
- import websockets
8
+ import webbrowser
13
9
 
14
10
  from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
15
11
  from GameSentenceMiner.obs import get_current_game
16
12
  from GameSentenceMiner.util.gsm_utils import TEXT_REPLACEMENTS_FILE
17
- from GameSentenceMiner.util.text_log import GameLine, get_line_by_id, initial_time, get_all_lines
18
- from flask import request, jsonify, send_from_directory
19
- import webbrowser
13
+ from GameSentenceMiner.util.text_log import get_line_by_id, get_all_lines
14
+ from flask import render_template, request, jsonify, send_from_directory
20
15
  from GameSentenceMiner import obs
21
- from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state, gsm_status
16
+ from GameSentenceMiner.util.configuration import logger, get_config, gsm_state, gsm_status
22
17
  from GameSentenceMiner.web.service import handle_texthooker_button
23
18
 
19
+ # Import from new modules
20
+ from GameSentenceMiner.web.events import (
21
+ EventItem, EventManager, EventProcessor, event_manager, event_queue, event_processor
22
+ )
23
+ from GameSentenceMiner.web.stats import (
24
+ is_kanji, interpolate_color, get_gradient_color, calculate_kanji_frequency,
25
+ calculate_heatmap_data, calculate_total_chars_per_game, calculate_reading_time_per_game,
26
+ calculate_reading_speed_per_game, generate_game_colors, format_large_number,
27
+ calculate_actual_reading_time, calculate_daily_reading_time, calculate_time_based_streak,
28
+ format_time_human_readable, calculate_current_game_stats, calculate_all_games_stats
29
+ )
30
+ from GameSentenceMiner.web.websockets import (
31
+ WebsocketServerThread, websocket_queue, paused, websocket_server_thread,
32
+ plaintext_websocket_server_thread, overlay_server_thread, websocket_server_threads,
33
+ handle_exit_signal
34
+ )
35
+ from GameSentenceMiner.web.database_api import register_database_api_routes
36
+
37
+ # Global configuration
24
38
  port = get_config().general.texthooker_port
25
39
  url = f"http://localhost:{port}"
26
40
  websocket_port = 55001
27
41
 
28
-
29
- @dataclass
30
- class EventItem:
31
- line: 'GameLine'
32
- id: str
33
- text: str
34
- time: datetime.datetime
35
- checked: bool = False
36
- history: bool = False
37
-
38
- def to_dict(self):
39
- return {
40
- 'id': self.id,
41
- 'text': self.text,
42
- 'time': self.time,
43
- 'checked': self.checked,
44
- 'history': self.history,
45
- }
46
-
47
- def to_serializable(self):
48
- return {
49
- 'id': self.id,
50
- 'text': self.text,
51
- 'time': self.time.isoformat(),
52
- 'checked': self.checked,
53
- 'history': self.history,
54
- }
55
-
56
-
57
- class EventManager:
58
- events: list[EventItem]
59
- events_dict: dict[str, EventItem] = {}
60
-
61
- def __init__(self):
62
- self.events = []
63
- self.ids = []
64
- self.events_dict = {}
65
- self._connect()
66
- self._create_table()
67
- self._load_events_from_db()
68
- # self.close_connection()
69
-
70
- def _connect(self):
71
- self.conn = sqlite3.connect(DB_PATH)
72
- self.cursor = self.conn.cursor()
73
-
74
- def _create_table(self):
75
- self.cursor.execute("""
76
- CREATE TABLE IF NOT EXISTS events (
77
- event_id TEXT PRIMARY KEY,
78
- line_id TEXT,
79
- text TEXT,
80
- time TEXT
81
- )
82
- """)
83
- self.conn.commit()
84
-
85
- def _load_events_from_db(self):
86
- self.cursor.execute("SELECT * FROM events")
87
- rows = self.cursor.fetchall()
88
- for row in rows:
89
- event_id, line_id, text, timestamp = row
90
- timestamp = datetime.datetime.fromisoformat(timestamp)
91
- line = GameLine(line_id, text, timestamp, None, None, 0)
92
- event = EventItem(line, event_id, text, timestamp,
93
- False, timestamp < initial_time)
94
- self.events.append(event)
95
- self.ids.append(event_id)
96
- self.events_dict[event_id] = event
97
-
98
- def __iter__(self):
99
- return iter(self.events)
100
-
101
- def replace_events(self, new_events: list[EventItem]):
102
- self.events = new_events
103
-
104
- def add_gameline(self, line: GameLine):
105
- new_event = EventItem(line, line.id, line.text,
106
- line.time, False, False)
107
- self.events_dict[line.id] = new_event
108
- self.ids.append(line.id)
109
- self.events.append(new_event)
110
- # self.store_to_db(new_event)
111
- # event_queue.put(new_event)
112
- return new_event
113
-
114
- def reset_checked_lines(self):
115
- for event in self.events:
116
- event.checked = False
117
-
118
- def get_events(self):
119
- return self.events
120
-
121
- def add_event(self, event):
122
- self.events.append(event)
123
- self.ids.append(event.id)
124
- event_queue.put(event)
125
-
126
- def get(self, event_id):
127
- return self.events_dict.get(event_id)
128
-
129
- def get_ids(self):
130
- return self.ids
131
-
132
- def close_connection(self):
133
- if self.conn:
134
- self.conn.close()
135
-
136
- def clear_history(self):
137
- self.cursor.execute("DELETE FROM events WHERE time < ?",
138
- (initial_time.isoformat(),))
139
- logger.info(f"Cleared history before {initial_time.isoformat()}")
140
- self.conn.commit()
141
- # Clear the in-memory events as well
142
- event_manager.events = [
143
- event for event in event_manager if not event.history]
144
- event_manager.events_dict = {
145
- event.id: event for event in event_manager.events}
146
-
147
-
148
- class EventProcessor(threading.Thread):
149
- def __init__(self, event_queue, db_path):
150
- super().__init__()
151
- self.event_queue = event_queue
152
- self.db_path = db_path
153
- self.conn = None
154
- self.cursor = None
155
- self.daemon = True
156
-
157
- def _connect(self):
158
- self.conn = sqlite3.connect(self.db_path)
159
- self.cursor = self.conn.cursor()
160
-
161
- def run(self):
162
- self._connect()
163
- while True:
164
- try:
165
- event = self.event_queue.get()
166
- if event is None: # Exit signal
167
- break
168
- self._store_to_db(event)
169
- except Exception as e:
170
- logger.error(f"Error processing event: {e}")
171
- self._close_connection()
172
-
173
- def _store_to_db(self, event):
174
- self.cursor.execute("""
175
- INSERT INTO events (event_id, line_id, text, time)
176
- VALUES (?, ?, ?, ?)
177
- """, (event.id, event.line.id, event.text, event.time.isoformat()))
178
- self.conn.commit()
179
-
180
- def _close_connection(self):
181
- if self.conn:
182
- self.conn.close()
183
-
184
-
185
- event_manager = EventManager()
186
- event_queue = queue.Queue()
187
-
188
- # Initialize the EventProcessor with the queue and event manager
189
- event_processor = EventProcessor(event_queue, DB_PATH)
190
- event_processor.start()
191
-
192
42
  server_start_time = datetime.datetime.now().timestamp()
193
43
 
194
44
  app = flask.Flask(__name__)
195
45
 
196
- # Load data from the JSON file
197
-
46
+ # Register database API routes
47
+ register_database_api_routes(app)
198
48
 
49
+ # Load data from the JSON file
199
50
  def load_data_from_file():
200
51
  if os.path.exists(TEXT_REPLACEMENTS_FILE):
201
52
  with open(TEXT_REPLACEMENTS_FILE, 'r', encoding='utf-8') as file:
@@ -203,8 +54,6 @@ def load_data_from_file():
203
54
  return {"enabled": True, "args": {"replacements": {}}}
204
55
 
205
56
  # Save data to the JSON file
206
-
207
-
208
57
  def save_data_to_file(data):
209
58
  with open(TEXT_REPLACEMENTS_FILE, 'w', encoding='utf-8') as file:
210
59
  json.dump(data, file, indent=4, ensure_ascii=False)
@@ -262,7 +111,19 @@ def texthooker():
262
111
 
263
112
  @app.route('/textreplacements')
264
113
  def textreplacements():
265
- return flask.render_template('text_replacements.html')
114
+ # Serve the text replacements data as JSON for compatibility
115
+ try:
116
+ if not os.path.exists(TEXT_REPLACEMENTS_FILE):
117
+ return jsonify({"error": "Text replacements file not found."}), 404
118
+ with open(TEXT_REPLACEMENTS_FILE, "r", encoding="utf-8") as f:
119
+ data = json.load(f)
120
+ return jsonify(data)
121
+ except Exception as e:
122
+ return jsonify({"error": f"Failed to load text replacements: {str(e)}"}), 500
123
+
124
+ @app.route('/database')
125
+ def database():
126
+ return flask.render_template('database.html')
266
127
 
267
128
 
268
129
  @app.route('/data', methods=['GET'])
@@ -283,7 +144,7 @@ def clear_history():
283
144
  return jsonify({'message': 'History cleared successfully'}), 200
284
145
 
285
146
 
286
- async def add_event_to_texthooker(line: GameLine):
147
+ async def add_event_to_texthooker(line):
287
148
  new_event = event_manager.add_gameline(line)
288
149
  await websocket_server_thread.send_text({
289
150
  'event': 'text_received',
@@ -389,32 +250,74 @@ Translate the following lines of game dialogue into natural-sounding, context-aw
389
250
  def get_status():
390
251
  return jsonify(gsm_status.to_dict()), 200
391
252
 
253
+ @app.template_filter('datetimeformat')
254
+ def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
255
+ """Formats a timestamp into a human-readable string."""
256
+ if value is None:
257
+ return ""
258
+ return datetime.datetime.fromtimestamp(float(value)).strftime(format)
259
+
260
+
261
+ @app.route('/stats')
262
+ def stats():
263
+ """Renders the stats page."""
264
+ return render_template('stats.html')
265
+
266
+ @app.route('/api/anki_stats')
267
+ def api_anki_stats():
268
+ """
269
+ API endpoint to provide Anki vs GSM kanji stats for the frontend.
270
+ Returns:
271
+ {
272
+ "missing_kanji": [ { "kanji": "漢", "frequency": 42 }, ... ],
273
+ "anki_kanji_count": 123,
274
+ "gsm_kanji_count": 456,
275
+ "coverage_percent": 27.0
276
+ }
277
+ """
278
+ from GameSentenceMiner.anki import get_all_anki_first_field_kanji
279
+ from GameSentenceMiner.web.stats import calculate_kanji_frequency, is_kanji
280
+ from GameSentenceMiner.util.db import GameLinesTable
281
+
282
+ # Get all GSM lines and calculate kanji frequency
283
+ all_lines = GameLinesTable.all()
284
+ gsm_kanji_stats = calculate_kanji_frequency(all_lines)
285
+ gsm_kanji_list = gsm_kanji_stats.get("kanji_data", [])
286
+ gsm_kanji_set = set([k["kanji"] for k in gsm_kanji_list])
287
+
288
+ # Get all kanji in Anki (first field only)
289
+ anki_kanji_set = get_all_anki_first_field_kanji()
290
+
291
+ # Find missing kanji (in GSM but not in Anki)
292
+ missing_kanji = [
293
+ {"kanji": k["kanji"], "frequency": k["frequency"]}
294
+ for k in gsm_kanji_list if k["kanji"] not in anki_kanji_set
295
+ ]
296
+
297
+ # Sort missing kanji by frequency descending
298
+ missing_kanji.sort(key=lambda x: x["frequency"], reverse=True)
299
+
300
+ # Coverage stats
301
+ anki_kanji_count = len(anki_kanji_set)
302
+ gsm_kanji_count = len(gsm_kanji_set)
303
+ coverage_percent = (anki_kanji_count / gsm_kanji_count * 100) if gsm_kanji_count else 0.0
304
+
305
+ return jsonify({
306
+ "missing_kanji": missing_kanji,
307
+ "anki_kanji_count": anki_kanji_count,
308
+ "gsm_kanji_count": gsm_kanji_count,
309
+ "coverage_percent": round(coverage_percent, 1)
310
+ })
311
+
312
+ @app.route('/search')
313
+ def search():
314
+ """Renders the search page."""
315
+ return render_template('search.html')
392
316
 
393
- # async def main():
394
- # async with websockets.serve(websocket_handler, "localhost", 8765): # Choose a port for WebSocket
395
- # print("WebSocket server started on ws://localhost:8765/ws (adjust as needed)")
396
- # await asyncio.Future() # Keep the server running
397
-
398
- # @app.route('/store-events', methods=['POST'])
399
- # def store_events():
400
- # data = request.get_json()
401
- # events_data = data.get('events', [])
402
- #
403
- # if not isinstance(events_data, list):
404
- # return jsonify({'error': 'Invalid data format. Expected an array of events.'}), 400
405
- #
406
- # for event_data in events_data:
407
- # if not all(k in event_data for k in ('id', 'text', 'time', 'checked')):
408
- # return jsonify({'error': 'Invalid event structure. Missing keys.'}), 400
409
- # if not (isinstance(event_data['id'], (int, float)) and
410
- # isinstance(event_data['text'], str) and
411
- # isinstance(event_data['time'], str) and
412
- # isinstance(event_data['checked'], bool)):
413
- # return jsonify({'error': 'Invalid event structure. Incorrect data types.'}), 400
414
- #
415
- # event_manager.replace_events([EventItem(item['id'], item['text'], item['time'], item.get(['timestamp'], 0), item['checked']) for item in data])
416
- #
417
- # return jsonify({'message': 'Events successfully stored on server.', 'receivedEvents': data}), 200
317
+ @app.route('/anki_stats')
318
+ def anki_stats():
319
+ """Renders the Anki statistics page."""
320
+ return render_template('anki_stats.html')
418
321
 
419
322
 
420
323
  def get_selected_lines():
@@ -452,117 +355,6 @@ def start_web_server():
452
355
  app.run(host='0.0.0.0', port=port, debug=False)
453
356
 
454
357
 
455
- websocket_queue = queue.Queue()
456
- paused = False
457
-
458
-
459
- class WebsocketServerThread(threading.Thread):
460
- def __init__(self, read, get_ws_port_func):
461
- super().__init__(daemon=True)
462
- self._loop = None
463
- self.read = read
464
- self.clients = set()
465
- self._event = threading.Event()
466
- self.get_ws_port_func = get_ws_port_func
467
- self.backedup_text = []
468
-
469
- @property
470
- def loop(self):
471
- self._event.wait()
472
- return self._loop
473
-
474
- async def send_text_coroutine(self, message):
475
- if not self.clients:
476
- self.backedup_text.append(message)
477
- return
478
- for client in self.clients:
479
- await client.send(message)
480
-
481
- async def server_handler(self, websocket):
482
- self.clients.add(websocket)
483
- try:
484
- if self.backedup_text:
485
- for message in self.backedup_text:
486
- await websocket.send(message)
487
- self.backedup_text.clear()
488
- async for message in websocket:
489
- if self.read and not paused:
490
- websocket_queue.put(message)
491
- try:
492
- await websocket.send('True')
493
- except websockets.exceptions.ConnectionClosedOK:
494
- pass
495
- else:
496
- try:
497
- await websocket.send('False')
498
- except websockets.exceptions.ConnectionClosedOK:
499
- pass
500
- except websockets.exceptions.ConnectionClosedError:
501
- pass
502
- finally:
503
- self.clients.remove(websocket)
504
-
505
- async def send_text(self, text):
506
- if text:
507
- if isinstance(text, dict) or isinstance(text, list):
508
- text = json.dumps(text)
509
- return asyncio.run_coroutine_threadsafe(
510
- self.send_text_coroutine(text), self.loop)
511
-
512
- def has_clients(self):
513
- return len(self.clients) > 0
514
-
515
- def stop_server(self):
516
- self.loop.call_soon_threadsafe(self._stop_event.set)
517
-
518
- def run(self):
519
- async def main():
520
- self._loop = asyncio.get_running_loop()
521
- self._stop_event = stop_event = asyncio.Event()
522
- self._event.set()
523
- while True:
524
- try:
525
- self.server = start_server = websockets.serve(self.server_handler,
526
- "0.0.0.0",
527
- self.get_ws_port_func(),
528
- max_size=1000000000)
529
- async with start_server:
530
- await stop_event.wait()
531
- return
532
- except Exception as e:
533
- logger.warning(
534
- f"WebSocket server encountered an error: {e}. Retrying...")
535
- await asyncio.sleep(1)
536
-
537
- asyncio.run(main())
538
-
539
-
540
- def handle_exit_signal(loop):
541
- logger.info("Received exit signal. Shutting down...")
542
- for task in asyncio.all_tasks(loop):
543
- task.cancel()
544
-
545
-
546
- websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda: get_config(
547
- ).get_field_value('advanced', 'texthooker_communication_websocket_port'))
548
- websocket_server_thread.start()
549
-
550
- if get_config().advanced.plaintext_websocket_port:
551
- plaintext_websocket_server_thread = WebsocketServerThread(
552
- read=False, get_ws_port_func=lambda: get_config().get_field_value('advanced', 'plaintext_websocket_port'))
553
- plaintext_websocket_server_thread.start()
554
-
555
- overlay_server_thread = WebsocketServerThread(
556
- read=False, get_ws_port_func=lambda: get_config().get_field_value('overlay', 'websocket_port'))
557
- overlay_server_thread.start()
558
-
559
- websocket_server_threads = [
560
- websocket_server_thread,
561
- plaintext_websocket_server_thread,
562
- overlay_server_thread
563
- ]
564
-
565
-
566
358
  async def texthooker_page_coro():
567
359
  global websocket_server_thread, plaintext_websocket_server_thread, overlay_server_thread
568
360
  # Run the WebSocket server in the asyncio event loop
@@ -581,4 +373,4 @@ def run_text_hooker_page():
581
373
 
582
374
 
583
375
  if __name__ == '__main__':
584
- asyncio.run(texthooker_page_coro())
376
+ asyncio.run(texthooker_page_coro())
@@ -0,0 +1,120 @@
1
+ import asyncio
2
+ import json
3
+ import queue
4
+ import threading
5
+ import websockets
6
+
7
+ from GameSentenceMiner.util.configuration import logger, get_config
8
+
9
+
10
+ websocket_queue = queue.Queue()
11
+ paused = False
12
+
13
+
14
+ class WebsocketServerThread(threading.Thread):
15
+ def __init__(self, read, get_ws_port_func):
16
+ super().__init__(daemon=True)
17
+ self._loop = None
18
+ self.read = read
19
+ self.clients = set()
20
+ self._event = threading.Event()
21
+ self.get_ws_port_func = get_ws_port_func
22
+ self.backedup_text = []
23
+
24
+ @property
25
+ def loop(self):
26
+ self._event.wait()
27
+ return self._loop
28
+
29
+ async def send_text_coroutine(self, message):
30
+ if not self.clients:
31
+ self.backedup_text.append(message)
32
+ return
33
+ for client in self.clients:
34
+ await client.send(message)
35
+
36
+ async def server_handler(self, websocket):
37
+ self.clients.add(websocket)
38
+ try:
39
+ if self.backedup_text:
40
+ for message in self.backedup_text:
41
+ await websocket.send(message)
42
+ self.backedup_text.clear()
43
+ async for message in websocket:
44
+ if self.read and not paused:
45
+ websocket_queue.put(message)
46
+ try:
47
+ await websocket.send('True')
48
+ except websockets.exceptions.ConnectionClosedOK:
49
+ pass
50
+ else:
51
+ try:
52
+ await websocket.send('False')
53
+ except websockets.exceptions.ConnectionClosedOK:
54
+ pass
55
+ except websockets.exceptions.ConnectionClosedError:
56
+ pass
57
+ finally:
58
+ self.clients.remove(websocket)
59
+
60
+ async def send_text(self, text):
61
+ if text:
62
+ if isinstance(text, dict) or isinstance(text, list):
63
+ text = json.dumps(text)
64
+ return asyncio.run_coroutine_threadsafe(
65
+ self.send_text_coroutine(text), self.loop)
66
+
67
+ def has_clients(self):
68
+ return len(self.clients) > 0
69
+
70
+ def stop_server(self):
71
+ self.loop.call_soon_threadsafe(self._stop_event.set)
72
+
73
+ def run(self):
74
+ async def main():
75
+ self._loop = asyncio.get_running_loop()
76
+ self._stop_event = stop_event = asyncio.Event()
77
+ self._event.set()
78
+ while True:
79
+ try:
80
+ self.server = start_server = websockets.serve(self.server_handler,
81
+ "0.0.0.0",
82
+ self.get_ws_port_func(),
83
+ max_size=1000000000)
84
+ async with start_server:
85
+ await stop_event.wait()
86
+ return
87
+ except Exception as e:
88
+ logger.warning(
89
+ f"WebSocket server encountered an error: {e}. Retrying...")
90
+ await asyncio.sleep(1)
91
+
92
+ asyncio.run(main())
93
+
94
+
95
+ def handle_exit_signal(loop):
96
+ logger.info("Received exit signal. Shutting down...")
97
+ for task in asyncio.all_tasks(loop):
98
+ task.cancel()
99
+
100
+
101
+ # Initialize WebSocket server threads
102
+ websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda: get_config(
103
+ ).get_field_value('advanced', 'texthooker_communication_websocket_port'))
104
+ websocket_server_thread.start()
105
+
106
+ plaintext_websocket_server_thread = None
107
+ if get_config().advanced.plaintext_websocket_port:
108
+ plaintext_websocket_server_thread = WebsocketServerThread(
109
+ read=False, get_ws_port_func=lambda: get_config().get_field_value('advanced', 'plaintext_websocket_port'))
110
+ plaintext_websocket_server_thread.start()
111
+
112
+ overlay_server_thread = WebsocketServerThread(
113
+ read=False, get_ws_port_func=lambda: get_config().get_field_value('overlay', 'websocket_port'))
114
+ overlay_server_thread.start()
115
+
116
+ websocket_server_threads = [
117
+ websocket_server_thread,
118
+ plaintext_websocket_server_thread,
119
+ overlay_server_thread
120
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.15.9
3
+ Version: 2.15.11
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