GameSentenceMiner 2.0.0__py3-none-any.whl → 2.0.1__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.
@@ -7,8 +7,6 @@ from . import configuration
7
7
  from . import obs
8
8
  from .configuration import *
9
9
 
10
- TOML_CONFIG_FILE = '../../config.toml'
11
- CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
12
10
  settings_saved = False
13
11
  on_save = []
14
12
 
@@ -186,7 +184,7 @@ class ConfigApp:
186
184
  self.master_config.set_config_for_profile(current_profile, config)
187
185
 
188
186
  # Serialize the config instance to JSON
189
- with open('../../config.json', 'w') as file:
187
+ with open(get_config_path(), 'w') as file:
190
188
  file.write(self.master_config.to_json(indent=4))
191
189
 
192
190
  print("Settings saved successfully!")
@@ -216,7 +216,7 @@ class ProfileConfig:
216
216
 
217
217
  self.anki.anki_custom_fields = config_data.get('anki_custom_fields', {})
218
218
 
219
- with open('config.json', 'w') as f:
219
+ with open(get_config_path(), 'w') as f:
220
220
  f.write(self.to_json(indent=4))
221
221
  print(
222
222
  'config.json successfully generated from previous settings. config.toml will no longer be used.')
@@ -247,38 +247,33 @@ class Config:
247
247
  def get_all_profile_names(self):
248
248
  return list(self.configs.keys())
249
249
 
250
-
251
- logger = logging.getLogger("GameSentenceMiner")
252
- logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
253
-
254
- # Create console handler with level INFO
255
- console_handler = logging.StreamHandler()
256
- console_handler.setLevel(logging.INFO)
257
-
258
- # Create rotating file handler with level DEBUG
259
- file_handler = RotatingFileHandler("gamesentenceminer.log", maxBytes=10_000_000, backupCount=2, encoding='utf-8')
260
- file_handler.setLevel(logging.DEBUG)
261
-
262
- # Create a formatter
263
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
264
-
265
- # Add formatter to handlers
266
- console_handler.setFormatter(formatter)
267
- file_handler.setFormatter(formatter)
268
-
269
- # Add handlers to the logger
270
- logger.addHandler(console_handler)
271
- logger.addHandler(file_handler)
272
-
273
- CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'get_config().json')
274
- temp_directory = ''
275
-
276
250
  def get_app_directory():
277
251
  appdata_dir = os.getenv('APPDATA') # Get the AppData directory
278
252
  config_dir = os.path.join(appdata_dir, 'GameSentenceMiner')
279
253
  os.makedirs(config_dir, exist_ok=True) # Create the directory if it doesn't exist
280
254
  return config_dir
281
255
 
256
+ def get_log_path():
257
+ return os.path.join(get_app_directory(), 'gamesentenceminer.log')
258
+
259
+ temp_directory = ''
260
+
261
+ def get_temporary_directory():
262
+ global temp_directory
263
+ if not temp_directory:
264
+ temp_directory = os.path.join(get_app_directory(), 'temp')
265
+ os.makedirs(temp_directory, exist_ok=True)
266
+ for filename in os.listdir(temp_directory):
267
+ file_path = os.path.join(temp_directory, filename)
268
+ try:
269
+ if os.path.isfile(file_path) or os.path.islink(file_path):
270
+ os.unlink(file_path)
271
+ elif os.path.isdir(file_path):
272
+ shutil.rmtree(file_path)
273
+ except Exception as e:
274
+ logger.error(f"Failed to delete {file_path}. Reason: {e}")
275
+ return temp_directory
276
+
282
277
  def get_config_path():
283
278
  return os.path.join(get_app_directory(), 'config.json')
284
279
 
@@ -354,6 +349,29 @@ def get_master_config():
354
349
  def switch_profile_and_save(profile_name):
355
350
  global config_instance
356
351
  config_instance.current_profile = profile_name
357
- with open('config.json', 'w') as file:
352
+ with open(get_config_path(), 'w') as file:
358
353
  json.dump(config_instance.to_dict(), file, indent=4)
359
354
  return config_instance.get_config()
355
+
356
+
357
+ logger = logging.getLogger("GameSentenceMiner")
358
+ logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
359
+
360
+ # Create console handler with level INFO
361
+ console_handler = logging.StreamHandler()
362
+ console_handler.setLevel(logging.INFO)
363
+
364
+ # Create rotating file handler with level DEBUG
365
+ file_handler = RotatingFileHandler(get_log_path(), maxBytes=10_000_000, backupCount=2, encoding='utf-8')
366
+ file_handler.setLevel(logging.DEBUG)
367
+
368
+ # Create a formatter
369
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
370
+
371
+ # Add formatter to handlers
372
+ console_handler.setFormatter(formatter)
373
+ file_handler.setFormatter(formatter)
374
+
375
+ # Add handlers to the logger
376
+ logger.addHandler(console_handler)
377
+ logger.addHandler(file_handler)
@@ -128,7 +128,7 @@ def get_audio_and_trim(video_path, line_time, next_line_time):
128
128
  codec_command = ["-c:a", f"{supported_formats[get_config().audio.extension]}"]
129
129
  logger.info(f"Re-encoding {codec} to {get_config().audio.extension}")
130
130
 
131
- untrimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.temp_directory,
131
+ untrimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
132
132
  suffix=f"_untrimmed.{get_config().audio.extension}").name
133
133
 
134
134
  command = ffmpeg_base_command_list + [
@@ -161,7 +161,7 @@ def get_video_duration(file_path):
161
161
 
162
162
 
163
163
  def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_line):
164
- trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.temp_directory,
164
+ trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
165
165
  suffix=f".{get_config().audio.extension}").name
166
166
  file_mod_time = get_file_modification_time(video_path)
167
167
  file_length = get_video_duration(video_path)
GameSentenceMiner/gsm.py CHANGED
@@ -105,7 +105,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
105
105
  def get_audio(line_time, next_line_time, video_path):
106
106
  trimmed_audio = get_audio_and_trim(video_path, line_time, next_line_time)
107
107
  vad_trimmed_audio = make_unique_file_name(
108
- f"{os.path.abspath(configuration.temp_directory)}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
108
+ f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
109
109
  final_audio_output = make_unique_file_name(
110
110
  f"{get_config().paths.audio_destination}{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
111
111
  should_update_audio = True
@@ -153,15 +153,6 @@ def initialize(reloading=False):
153
153
  os.mkdir(get_config().paths.screenshot_destination)
154
154
  if not os.path.exists(get_config().paths.audio_destination):
155
155
  os.mkdir(get_config().paths.audio_destination)
156
- if not os.path.exists("../temp_files"):
157
- os.mkdir("../temp_files")
158
- else:
159
- for filename in os.scandir('../temp_files'):
160
- file_path = os.path.join('../temp_files', filename.name)
161
- if filename.is_file() or filename.is_symlink():
162
- os.remove(file_path)
163
- elif filename.is_dir():
164
- shutil.rmtree(file_path)
165
156
  if get_config().vad.do_vad_postprocessing:
166
157
  if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
167
158
  vosk_helper.get_vosk_model()
@@ -349,36 +340,34 @@ def main(reloading=False, do_config_input=True):
349
340
  global settings_window
350
341
  logger.info("Script started.")
351
342
  initialize(reloading)
352
- with tempfile.TemporaryDirectory(dir="../temp_files") as temp_dir:
353
- configuration.temp_directory = temp_dir
354
- event_handler = VideoToAudioHandler()
355
- observer = Observer()
356
- observer.schedule(event_handler, get_config().paths.folder_to_watch, recursive=False)
357
- observer.start()
343
+ event_handler = VideoToAudioHandler()
344
+ observer = Observer()
345
+ observer.schedule(event_handler, get_config().paths.folder_to_watch, recursive=False)
346
+ observer.start()
358
347
 
359
- logger.info("Script Initialized. Happy Mining!")
360
- if not is_linux():
361
- register_hotkeys()
348
+ logger.info("Script Initialized. Happy Mining!")
349
+ if not is_linux():
350
+ register_hotkeys()
362
351
 
363
- # Register signal handlers for graceful shutdown
364
- signal.signal(signal.SIGTERM, handle_exit()) # Handle `kill` commands
365
- signal.signal(signal.SIGINT, handle_exit()) # Handle Ctrl+C
366
- win32api.SetConsoleCtrlHandler(handle_exit())
352
+ # Register signal handlers for graceful shutdown
353
+ signal.signal(signal.SIGTERM, handle_exit()) # Handle `kill` commands
354
+ signal.signal(signal.SIGINT, handle_exit()) # Handle Ctrl+C
355
+ win32api.SetConsoleCtrlHandler(handle_exit())
367
356
 
368
- util.run_new_thread(run_tray)
357
+ util.run_new_thread(run_tray)
369
358
 
370
- try:
371
- settings_window = config_gui.ConfigApp()
372
- settings_window.add_save_hook(update_icon)
373
- settings_window.window.mainloop()
374
- except KeyboardInterrupt:
375
- cleanup()
359
+ try:
360
+ settings_window = config_gui.ConfigApp()
361
+ settings_window.add_save_hook(update_icon)
362
+ settings_window.window.mainloop()
363
+ except KeyboardInterrupt:
364
+ cleanup()
376
365
 
377
- try:
378
- observer.stop()
379
- observer.join()
380
- except Exception as e:
381
- logger.error(f"Error stopping observer: {e}")
366
+ try:
367
+ observer.stop()
368
+ observer.join()
369
+ except Exception as e:
370
+ logger.error(f"Error stopping observer: {e}")
382
371
 
383
372
 
384
373
  if __name__ == "__main__":
GameSentenceMiner/obs.py CHANGED
@@ -1,8 +1,7 @@
1
1
  import time
2
+ from sys import platform
2
3
 
3
- import obswebsocket
4
4
  from obswebsocket import obsws, requests
5
- from obswebsocket.exceptions import ConnectionFailure
6
5
 
7
6
  from . import util
8
7
  from . import configuration
@@ -14,6 +13,47 @@ client: obsws = None
14
13
  # REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
15
14
 
16
15
 
16
+ def get_obs_websocket_config_values():
17
+ if platform == "win32":
18
+ config_path = os.path.expanduser(r"~\AppData\Roaming\obs-studio\plugin_config\obs-websocket\config.json")
19
+ elif platform == "darwin": # macOS
20
+ config_path = os.path.expanduser(
21
+ "~/Library/Application Support/obs-studio/plugin_config/obs-websocket/config.json")
22
+ elif platform == "linux":
23
+ config_path = os.path.expanduser("~/.config/obs-studio/plugin_config/obs-websocket/config.json")
24
+ else:
25
+ raise Exception("Unsupported operating system.")
26
+
27
+ # Check if config file exists
28
+ if not os.path.isfile(config_path):
29
+ raise FileNotFoundError(f"OBS WebSocket config not found at {config_path}")
30
+
31
+ # Read the JSON configuration
32
+ with open(config_path, 'r') as file:
33
+ config = json.load(file)
34
+
35
+ # Extract values
36
+ server_enabled = config.get("server_enabled", False)
37
+ server_port = config.get("server_port", 4455) # Default to 4455 if not set
38
+ server_password = config.get("server_password", None)
39
+
40
+ if not server_enabled:
41
+ logger.info("OBS WebSocket server is not enabled. Enabling it now... Restart OBS for changes to take effect.")
42
+ config["server_enabled"] = True
43
+
44
+ with open(config_path, 'w') as file:
45
+ json.dump(config, file, indent=4)
46
+
47
+ if get_config().obs.password == 'your_password':
48
+ logger.info("OBS WebSocket password is not set. Setting it now...")
49
+ config = get_master_config()
50
+ config.get_config().obs.port = server_port
51
+ config.get_config().obs.password = server_password
52
+ with open(get_config_path(), 'w') as file:
53
+ json.dump(config.to_dict(), file, indent=4)
54
+ reload_config()
55
+
56
+
17
57
  def on_connect(obs):
18
58
  logger.info("Connected to OBS WebSocket.")
19
59
  time.sleep(2)
@@ -28,14 +68,11 @@ def on_disconnect(obs):
28
68
  def connect_to_obs(start_replay=False):
29
69
  global client
30
70
  if get_config().obs.enabled:
71
+ get_obs_websocket_config_values()
31
72
  client = obsws(host=get_config().obs.host, port=get_config().obs.port,
32
73
  password=get_config().obs.password, authreconnect=1, on_connect=on_connect,
33
74
  on_disconnect=on_disconnect)
34
- try:
35
- client.connect()
36
- except ConnectionFailure:
37
- logger.error("OBS Websocket Connection Has not been Set up, please set it up in Settings")
38
- exit(1)
75
+ client.connect()
39
76
 
40
77
  time.sleep(1)
41
78
  if start_replay and get_config().obs.start_buffer:
@@ -101,7 +138,7 @@ def get_source_from_scene(scene_name):
101
138
 
102
139
  def get_screenshot():
103
140
  try:
104
- screenshot = util.make_unique_file_name(os.path.abspath(configuration.temp_directory) + '/screenshot.png')
141
+ screenshot = util.make_unique_file_name(os.path.abspath(configuration.get_temporary_directory()) + '/screenshot.png')
105
142
  update_current_game()
106
143
  current_source = get_source_from_scene(get_current_game())
107
144
  current_source_name = current_source.sourceName
@@ -12,7 +12,7 @@ vad_model = load_silero_vad()
12
12
  # Use Silero to detect voice activity with timestamps in the audio
13
13
  def detect_voice_with_silero(input_audio):
14
14
  # Convert the audio to 16kHz mono WAV
15
- temp_wav = tempfile.NamedTemporaryFile(dir=configuration.temp_directory, suffix='.wav').name
15
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
16
16
  ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
17
17
 
18
18
  # Load the audio and detect speech timestamps
@@ -67,7 +67,7 @@ def download_and_cache_vosk_model(model_dir="vosk_model_cache"):
67
67
  def detect_voice_with_vosk(input_audio):
68
68
  global vosk_model_path, vosk_model
69
69
  # Convert the audio to 16kHz mono WAV
70
- temp_wav = tempfile.NamedTemporaryFile(dir=configuration.temp_directory, suffix='.wav').name
70
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
71
71
  ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
72
72
 
73
73
  if not vosk_model_path or not vosk_model:
@@ -24,7 +24,7 @@ def load_whisper_model():
24
24
  # Use Whisper to detect voice activity with timestamps in the audio
25
25
  def detect_voice_with_whisper(input_audio):
26
26
  # Convert the audio to 16kHz mono WAV
27
- temp_wav = tempfile.NamedTemporaryFile(dir=configuration.temp_directory, suffix='.wav').name
27
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
28
28
  ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
29
29
 
30
30
  # Make sure Whisper is loaded
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: GameSentenceMiner
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -38,6 +38,12 @@ Requires-Dist: pywin32
38
38
 
39
39
  # Sentence Mining Game Audio Trim Helper
40
40
 
41
+ ## WARNING
42
+
43
+ This project is in process of a major rewrite, and this README will be updated soon with the new process.
44
+
45
+ ---
46
+
41
47
  This project automates the recording of game sentence audio to help with Anki Card Creation.
42
48
 
43
49
  This allows us to create cards from texthooker/yomitan, and automatically get screenshot and sentence audio from the
@@ -280,7 +286,7 @@ Screenshots to help with setup:
280
286
 
281
287
  1. Start game
282
288
  2. Hook Game with Agent (or textractor) with clipboard enabled
283
- 3. start script: `python main.py`
289
+ 3. start script: `python -m src.GameSentenceMiner.gsm`
284
290
  1. Create Anki Card with target word (through a texthooker page/Yomitan)
285
291
  2. (If full-auto-mode not on) Trigger Hotkey to record replay buffer
286
292
  4. When finished gaming, end script
@@ -0,0 +1,20 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=3UT6K5PxzJMDoiXyOULrkeCoJ0KMq7JvBw6XhcLCirE,9114
3
+ GameSentenceMiner/config_gui.py,sha256=s03e8F2pEPj0iG31RxFqWGfm2GLb8JoGQR7QhCMFu3M,45581
4
+ GameSentenceMiner/configuration.py,sha256=7sXYNBNXyezPFbGKTu2XwjMAy6vQ1tJPfLg2aq2UGRA,13859
5
+ GameSentenceMiner/ffmpeg.py,sha256=hdKimzkpAKsE-17qEAQg4uHy4-TtdFywYx48Skn9cPs,10418
6
+ GameSentenceMiner/gametext.py,sha256=GpR9P8h3GmmKH46Dw13kJPx66n3jGjFCiV8Fcrqn9E8,3999
7
+ GameSentenceMiner/gsm.py,sha256=Lnj59KHV4l5eEgffkswBlRCONgYB_Id3L0pTajUzsvI,14937
8
+ GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
9
+ GameSentenceMiner/notification.py,sha256=sWgIIXhaB9WV1K_oQGf5-IR6q3dakae_QS-RuIvbcEs,1939
10
+ GameSentenceMiner/obs.py,sha256=gutnRk30jIxuvrpEosR9jw2h_tll36wgtAZkUF-vWDY,5256
11
+ GameSentenceMiner/util.py,sha256=OYg0j_rT9F7v3aJRwWnHvdWMYyxGlimrvw7U2C9ifeY,4441
12
+ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ GameSentenceMiner/vad/silero_trim.py,sha256=r7bZYEj-NUXGKgD2UIhLrbTPyq0rau97qGtrMZcRK4A,1517
14
+ GameSentenceMiner/vad/vosk_helper.py,sha256=lWmlGMhmg_0QoWeCHrXwz9wDKPqY37BckHCekGVtJUI,5794
15
+ GameSentenceMiner/vad/whisper_helper.py,sha256=9kmPeSs6jRcKSVxYY-vtyTcqVUDORR4q1nl8fqxHHn4,3379
16
+ GameSentenceMiner-2.0.1.dist-info/METADATA,sha256=1Do_9ICUOyK_eo89olnaBQhg-yHyLEDtyky8NXh4-_8,13534
17
+ GameSentenceMiner-2.0.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
+ GameSentenceMiner-2.0.1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
19
+ GameSentenceMiner-2.0.1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
20
+ GameSentenceMiner-2.0.1.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=3UT6K5PxzJMDoiXyOULrkeCoJ0KMq7JvBw6XhcLCirE,9114
3
- GameSentenceMiner/config_gui.py,sha256=d1UDXbTNSZBFneWEytqdnySBL-_iJ1MGY0Jvyg3vn80,45691
4
- GameSentenceMiner/configuration.py,sha256=ilTnUXFIu9G7folA4AT78M57Wq9-j74gi5MbYWDtDRw,13165
5
- GameSentenceMiner/ffmpeg.py,sha256=XreJHjmCpmdI04g_UmTzXmh5pay1s5DLx41nrAJmQwo,10396
6
- GameSentenceMiner/gametext.py,sha256=GpR9P8h3GmmKH46Dw13kJPx66n3jGjFCiV8Fcrqn9E8,3999
7
- GameSentenceMiner/gsm.py,sha256=4kOb3eIiOZLpRv-ZhczoJ_ZtqMMxnxK0XW9gLWkdK8I,15557
8
- GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
9
- GameSentenceMiner/notification.py,sha256=sWgIIXhaB9WV1K_oQGf5-IR6q3dakae_QS-RuIvbcEs,1939
10
- GameSentenceMiner/obs.py,sha256=-hGz3D9roCDVecZ9IhIofPppcmTEAYO8cZf-lnkBBxU,3687
11
- GameSentenceMiner/util.py,sha256=OYg0j_rT9F7v3aJRwWnHvdWMYyxGlimrvw7U2C9ifeY,4441
12
- GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- GameSentenceMiner/vad/silero_trim.py,sha256=x1jTQF9x55Q2HF32xAE4QrLoa30U3ksOmY9XF_qulrs,1506
14
- GameSentenceMiner/vad/vosk_helper.py,sha256=6vPkfaj2TJtS0Ph3aUByTx98rb-InKcnhp7MZh-vY-c,5783
15
- GameSentenceMiner/vad/whisper_helper.py,sha256=sfGTXU3dt5ZAXMaZbBp3j2cvYc0QF-HXD72_Lgl1NTs,3368
16
- GameSentenceMiner-2.0.0.dist-info/METADATA,sha256=m4DB-eNUyyaygdHyAmO7gZVyWcXQijPtxlPc6AVPKzI,13389
17
- GameSentenceMiner-2.0.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
- GameSentenceMiner-2.0.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
19
- GameSentenceMiner-2.0.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
20
- GameSentenceMiner-2.0.0.dist-info/RECORD,,