GameSentenceMiner 2.18.3__py3-none-any.whl → 2.18.5__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.

Potentially problematic release.


This version of GameSentenceMiner might be problematic. Click here for more details.

GameSentenceMiner/obs.py CHANGED
@@ -132,11 +132,14 @@ class OBSConnectionManager(threading.Thread):
132
132
 
133
133
  def check_replay_buffer_enabled(self):
134
134
  if not self.should_check_output:
135
- return True, ""
136
- output = get_replay_buffer_output()
137
- if not output:
138
- return False, "Replay Buffer output not found in OBS. Please enable Replay Buffer In OBS Settings -> Output -> Replay Buffer. I recommend 300 seconds (5 minutes) or higher."
139
- return True, ""
135
+ return 300, ""
136
+ buffer_seconds = get_replay_buffer_max_time_seconds()
137
+ if not buffer_seconds:
138
+ replay_output = get_replay_buffer_output()
139
+ if not replay_output:
140
+ return 0, "Replay Buffer output not found in OBS. Please enable Replay Buffer In OBS Settings -> Output -> Replay Buffer. I recommend 300 seconds (5 minutes) or higher."
141
+ return 300, ""
142
+ return buffer_seconds, ""
140
143
 
141
144
  def _manage_replay_buffer_and_utils(self):
142
145
  errors = []
@@ -150,11 +153,13 @@ class OBSConnectionManager(threading.Thread):
150
153
  errors.append("Automatic Replay Buffer management is disabled in GSM settings.")
151
154
  return errors
152
155
 
153
- replay_buffer_enabled, error_message = self.check_replay_buffer_enabled()
156
+ buffer_seconds, error_message = self.check_replay_buffer_enabled()
154
157
 
155
- if not replay_buffer_enabled:
158
+ if not buffer_seconds:
156
159
  errors.append(error_message)
157
160
  return errors
161
+
162
+ self.NO_OUTPUT_SHUTDOWN_SECONDS = max(300, buffer_seconds * 1.10) # At least 5 minutes or 10% more than buffer
158
163
 
159
164
  current_status = get_replay_buffer_status()
160
165
 
@@ -167,10 +172,10 @@ class OBSConnectionManager(threading.Thread):
167
172
  self.no_output_timestamp = None
168
173
  return errors
169
174
 
170
- img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
171
- has_changed = self.has_image_changed(img) if img else True
175
+ img = get_screenshot_PIL(compression=75, img_format='jpg', width=1280, height=720)
176
+ is_empty = self.is_image_empty(img) if img else True
172
177
 
173
- if not has_changed:
178
+ if not is_empty:
174
179
  self.no_output_timestamp = None
175
180
  if not current_status:
176
181
  start_replay_buffer()
@@ -184,20 +189,18 @@ class OBSConnectionManager(threading.Thread):
184
189
  self.last_replay_buffer_status = False
185
190
  self.no_output_timestamp = None
186
191
 
187
- def has_image_changed(self, img):
188
- if self.previous_image is None:
189
- self.previous_image = np.array(img)
190
- return True
192
+ def is_image_empty(self, img):
191
193
  try:
192
- img1_np = np.array(img) if not isinstance(img, np.ndarray) else img
193
- img2_np = self.previous_image
194
- self.previous_image = img1_np
194
+ extrema = img.getextrema()
195
+ if isinstance(extrema[0], tuple):
196
+ is_empty = all(e[0] == e[1] for e in extrema)
197
+ else:
198
+ is_empty = extrema[0] == extrema[1]
199
+ return is_empty
195
200
  except Exception:
196
- logger.warning("Failed to convert images to numpy arrays for comparison.")
201
+ logger.warning("Failed to check image extrema for emptiness.")
197
202
  return False
198
203
 
199
- return (img1_np.shape == img2_np.shape) and np.array_equal(img1_np, img2_np)
200
-
201
204
  def run(self):
202
205
  time.sleep(5) # Initial delay to allow OBS to start
203
206
  while self.running:
@@ -566,7 +569,7 @@ def get_replay_buffer_max_time_seconds():
566
569
  logger.warning(f"get_output_settings for replay_buffer failed: {response.status}")
567
570
  return 0
568
571
  except Exception as e:
569
- logger.error(f"Exception while fetching replay buffer settings: {e}")
572
+ # logger.error(f"Exception while fetching replay buffer settings: {e}")
570
573
  return 0
571
574
 
572
575
  def enable_replay_buffer():
@@ -805,7 +808,24 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
805
808
  except obs.error.OBSSDKError as e:
806
809
  logger.error(f"An OBS error occurred: {e}")
807
810
  except Exception as e:
808
- logger.error(f"An unexpected error occurred: {e}")
811
+ logger.error(f"An unexpected error occurred: {e}")
812
+
813
+ def get_current_source_input_settings():
814
+ with connection_pool.get_client() as client:
815
+ client: obs.ReqClient
816
+ current_scene = get_current_scene()
817
+ if not current_scene:
818
+ return None
819
+ scene_items_response = client.get_scene_item_list(name=current_scene)
820
+ items = scene_items_response.scene_items if scene_items_response and scene_items_response.scene_items else []
821
+ if not items:
822
+ return None
823
+ first_item = items[0]
824
+ source_name = first_item.get('sourceName')
825
+ if not source_name:
826
+ return None
827
+ input_settings_response = client.get_input_settings(name=source_name)
828
+ return input_settings_response.input_settings if input_settings_response else None
809
829
 
810
830
 
811
831
  def main():
@@ -867,17 +887,23 @@ if __name__ == '__main__':
867
887
  logging.basicConfig(level=logging.INFO)
868
888
  connect_to_obs_sync()
869
889
 
870
- save_replay_buffer()
890
+ # outputs = get_output_list()
891
+ # print(outputs)
892
+
893
+ # output = get_replay_buffer_output()
894
+ # print(output)
895
+
896
+ # save_replay_buffer()
871
897
  # img = get_screenshot_PIL(source_name='Display Capture 2', compression=100, img_format='jpg', width=2560, height=1440)
872
898
  # img.show()
873
- # output_list = get_output_list()
874
- # print(output_list)
899
+ # source = get_current_source_input_settings()
900
+ # print(source)
875
901
 
876
902
  # response = enable_replay_buffer()
877
903
  # print(response)
878
904
 
879
905
  # response = get_replay_buffer_max_time_seconds()
880
- # # response is dataclass with attributes, print attributes
906
+ # response is dataclass with attributes, print attributes
881
907
  # print(response)
882
908
 
883
909
  # response = enable_replay_buffer()
@@ -93,6 +93,7 @@ class SQLiteDBTable:
93
93
  _types: List[type] = []
94
94
  _pk: str = 'id'
95
95
  _auto_increment: bool = True
96
+ _column_order_cache: Optional[List[str]] = None # Cache for actual column order
96
97
 
97
98
  def __init_subclass__(cls, **kwargs):
98
99
  super().__init_subclass__(**kwargs)
@@ -104,6 +105,7 @@ class SQLiteDBTable:
104
105
  @classmethod
105
106
  def set_db(cls, db: SQLiteDB):
106
107
  cls._db = db
108
+ cls._column_order_cache = None # Reset cache when database changes
107
109
  # Ensure table exists
108
110
  if not db.table_exists(cls._table):
109
111
  fields_def = ', '.join([f"{field} TEXT" for field in cls._fields])
@@ -115,6 +117,7 @@ class SQLiteDBTable:
115
117
  for field in cls._fields:
116
118
  if field not in existing_columns:
117
119
  db.execute(f"ALTER TABLE {cls._table} ADD COLUMN {field} TEXT", commit=True)
120
+ cls._column_order_cache = None # Reset cache when schema changes
118
121
 
119
122
  @classmethod
120
123
  def all(cls: Type[T]) -> List[T]:
@@ -137,49 +140,80 @@ class SQLiteDBTable:
137
140
  if not row:
138
141
  return None
139
142
  obj = cls()
140
- fields = [cls._pk] + cls._fields
141
- for i, field in enumerate(fields):
142
- if i == 0 and field == cls._pk:
143
- if cls._types[i] is int:
144
- setattr(obj, field, int(row[i])
145
- if row[i] is not None else None)
146
- elif cls._types[i] is str:
147
- setattr(obj, field, str(row[i])
148
- if row[i] is not None else None)
149
- continue
150
- if cls._types[i] is str:
151
- if not row[i]:
152
- setattr(obj, field, "")
153
- elif (row[i].startswith('[') or row[i].startswith('{')):
154
- try:
155
- setattr(obj, field, json.loads(row[i]))
156
- except json.JSONDecodeError:
157
- setattr(obj, field, row[i])
158
- else:
159
- setattr(obj, field, str(row[i])
160
- if row[i] is not None else None)
161
- elif cls._types[i] is list:
162
- try:
163
- setattr(obj, field, json.loads(row[i]) if row[i] else [])
164
- except json.JSONDecodeError:
165
- setattr(obj, field, [])
166
- elif cls._types[i] is int:
167
- setattr(obj, field, int(row[i])
168
- if row[i] is not None else None)
169
- elif cls._types[i] is float:
170
- setattr(obj, field, float(row[i])
171
- if row[i] is not None else None)
172
- elif cls._types[i] is bool:
173
- setattr(obj, field, bool(row[i])
174
- if row[i] is not None else None)
175
- elif cls._types[i] is dict:
143
+
144
+ try:
145
+ # Get actual column order from database schema
146
+ actual_columns = cls.get_actual_column_order()
147
+ expected_fields = [cls._pk] + cls._fields
148
+
149
+ # Create a mapping from actual column positions to expected field positions
150
+ column_mapping = {}
151
+ for i, actual_col in enumerate(actual_columns):
152
+ if actual_col in expected_fields:
153
+ expected_index = expected_fields.index(actual_col)
154
+ column_mapping[i] = expected_index
155
+
156
+ # Process each column in the row based on the mapping
157
+ for actual_pos, row_value in enumerate(row):
158
+ if actual_pos not in column_mapping:
159
+ continue # Skip unknown columns
160
+
161
+ expected_pos = column_mapping[actual_pos]
162
+ field = expected_fields[expected_pos]
163
+ field_type = cls._types[expected_pos]
164
+
165
+ cls._set_field_value(obj, field, field_type, row_value, expected_pos == 0 and field == cls._pk)
166
+
167
+ except Exception as e:
168
+ # Fallback to original behavior if schema-based mapping fails
169
+ logger.warning(f"Column mapping failed for {cls._table}, falling back to positional mapping: {e}")
170
+ expected_fields = [cls._pk] + cls._fields
171
+ for i, field in enumerate(expected_fields):
172
+ if i >= len(row):
173
+ break # Safety check
174
+ field_type = cls._types[i]
175
+ cls._set_field_value(obj, field, field_type, row[i], i == 0 and field == cls._pk)
176
+
177
+ return obj
178
+
179
+ @classmethod
180
+ def _set_field_value(cls, obj, field: str, field_type: type, row_value, is_pk: bool = False):
181
+ """Helper method to set field value with proper type conversion."""
182
+ if is_pk:
183
+ if field_type is int:
184
+ setattr(obj, field, int(row_value) if row_value is not None else None)
185
+ elif field_type is str:
186
+ setattr(obj, field, str(row_value) if row_value is not None else None)
187
+ return
188
+
189
+ if field_type is str:
190
+ if not row_value:
191
+ setattr(obj, field, "")
192
+ elif isinstance(row_value, str) and (row_value.startswith('[') or row_value.startswith('{')):
176
193
  try:
177
- setattr(obj, field, json.loads(row[i]) if row[i] else {})
194
+ setattr(obj, field, json.loads(row_value))
178
195
  except json.JSONDecodeError:
179
- setattr(obj, field, {})
196
+ setattr(obj, field, row_value)
180
197
  else:
181
- setattr(obj, field, row[i])
182
- return obj
198
+ setattr(obj, field, str(row_value) if row_value is not None else None)
199
+ elif field_type is list:
200
+ try:
201
+ setattr(obj, field, json.loads(row_value) if row_value else [])
202
+ except json.JSONDecodeError:
203
+ setattr(obj, field, [])
204
+ elif field_type is int:
205
+ setattr(obj, field, int(row_value) if row_value is not None else None)
206
+ elif field_type is float:
207
+ setattr(obj, field, float(row_value) if row_value is not None else None)
208
+ elif field_type is bool:
209
+ setattr(obj, field, bool(row_value) if row_value is not None else None)
210
+ elif field_type is dict:
211
+ try:
212
+ setattr(obj, field, json.loads(row_value) if row_value else {})
213
+ except json.JSONDecodeError:
214
+ setattr(obj, field, {})
215
+ else:
216
+ setattr(obj, field, row_value)
183
217
 
184
218
  def save(self, retry=1):
185
219
  try:
@@ -245,9 +279,9 @@ class SQLiteDBTable:
245
279
 
246
280
  def add_column(self, column_name: str, new_column_type: str = "TEXT"):
247
281
  try:
248
- index = self._fields.index(column_name) + 1
249
282
  self._db.execute(
250
283
  f"ALTER TABLE {self._table} ADD COLUMN {column_name} {new_column_type}", commit=True)
284
+ self.__class__._column_order_cache = None # Reset cache when schema changes
251
285
  logger.info(f"Added column {column_name} to {self._table}")
252
286
  except sqlite3.OperationalError as e:
253
287
  if "duplicate column name" in str(e):
@@ -286,11 +320,13 @@ class SQLiteDBTable:
286
320
  def rename_column(cls, old_column: str, new_column: str):
287
321
  cls._db.execute(
288
322
  f"ALTER TABLE {cls._table} RENAME COLUMN {old_column} TO {new_column}", commit=True)
323
+ cls._column_order_cache = None # Reset cache when schema changes
289
324
 
290
325
  @classmethod
291
326
  def drop_column(cls, column_name: str):
292
327
  cls._db.execute(
293
328
  f"ALTER TABLE {cls._table} DROP COLUMN {column_name}", commit=True)
329
+ cls._column_order_cache = None # Reset cache when schema changes
294
330
 
295
331
  @classmethod
296
332
  def get_column_type(cls, column_name: str) -> Optional[str]:
@@ -315,6 +351,36 @@ class SQLiteDBTable:
315
351
  f"UPDATE {cls._table} SET {new_column} = CAST({old_column} AS {new_type})", commit=True)
316
352
  cls._db.execute(
317
353
  f"ALTER TABLE {cls._table} DROP COLUMN {old_column}", commit=True)
354
+ cls._column_order_cache = None # Reset cache when schema changes
355
+
356
+ @classmethod
357
+ def get_actual_column_order(cls) -> List[str]:
358
+ """Get the actual column order from the database schema."""
359
+ if cls._column_order_cache is not None:
360
+ return cls._column_order_cache
361
+
362
+ # Use direct database access to avoid recursion through from_row()
363
+ with cls._db._lock:
364
+ import sqlite3
365
+ with sqlite3.connect(cls._db.db_path, check_same_thread=False) as conn:
366
+ cursor = conn.cursor()
367
+ cursor.execute(f"PRAGMA table_info({cls._table})")
368
+ columns_info = cursor.fetchall()
369
+
370
+ # Each row is (cid, name, type, notnull, dflt_value, pk)
371
+ # Sort by column id (cid) to get the actual order
372
+ sorted_columns = sorted(columns_info, key=lambda x: x[0])
373
+ column_order = [col[1] for col in sorted_columns]
374
+
375
+ # Cache the result
376
+ cls._column_order_cache = column_order
377
+ return column_order
378
+
379
+ @classmethod
380
+ def get_expected_column_list(cls) -> str:
381
+ """Get comma-separated list of columns in expected order for explicit SELECT queries."""
382
+ expected_fields = [cls._pk] + cls._fields
383
+ return ', '.join(expected_fields)
318
384
 
319
385
 
320
386
  class AIModelsTable(SQLiteDBTable):
@@ -1182,7 +1182,7 @@ def register_database_api_routes(app):
1182
1182
  })
1183
1183
 
1184
1184
  except Exception as e:
1185
- logger.error(f"Unexpected error in api_stats: {e}")
1185
+ logger.error(f"Unexpected error in api_stats: {e}", exc_info=True)
1186
1186
  return jsonify({'error': 'Failed to generate statistics'}), 500
1187
1187
 
1188
1188
  @app.route('/api/goals-today', methods=['GET'])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.18.3
3
+ Version: 2.18.5
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
@@ -2,7 +2,7 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
2
2
  GameSentenceMiner/anki.py,sha256=TgLJIGZq6qPfmXV378BzG_SEnL6KvHK74u4ztTggzbM,31139
3
3
  GameSentenceMiner/gametext.py,sha256=FBL3kgJ71hCg5Nczuo9dAEi_sLGdVIGgvc62bT5KhCc,10691
4
4
  GameSentenceMiner/gsm.py,sha256=0hEpEBDbI9FtiKtHeyrSLKV1nys-mKTKfxLY0Dk7mOQ,36387
5
- GameSentenceMiner/obs.py,sha256=R2Rsq2rMnWIoIb4tArTY7oAFIsyxfp1Nf-XnX_E95io,35858
5
+ GameSentenceMiner/obs.py,sha256=Ru0523AYqJhZG3oKrDzw-7dQszCRjAKgS06yV5fKvcs,36886
6
6
  GameSentenceMiner/vad.py,sha256=dhZpbTsVI4scqXZ0M701BenUur0EzYM96tnyGXo8iI0,21021
7
7
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  GameSentenceMiner/ai/ai_prompting.py,sha256=mq9Odv_FpohXagU-OoSZbLWttdrEl1M1NiqnodeUpD8,29126
@@ -41,7 +41,7 @@ GameSentenceMiner/ui/config_gui.py,sha256=Nt6uuJrinOrSpDdrw92zdd1GvyQNjOoGvkZQdG
41
41
  GameSentenceMiner/ui/screenshot_selector.py,sha256=AKML87MpgYQeSuj1F10GngpNrn9qp06zLLzNRwrQWM8,8900
42
42
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  GameSentenceMiner/util/configuration.py,sha256=fYlU4Qrr6T-k_G2MY2rgoQ9EV_k7XBfbtsO8q7PKTsU,46653
44
- GameSentenceMiner/util/db.py,sha256=3Fs75pGJyf8-3y3k6Zfpgp8z9tYL9-ZvQUk_FgDc9Vw,28664
44
+ GameSentenceMiner/util/db.py,sha256=1DjGjlwWnPefmQfzvMqqFPW0a0qeO-fIXE1YqKiok18,32000
45
45
  GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
46
46
  GameSentenceMiner/util/ffmpeg.py,sha256=cAzztfY36Xf2WvsJDjavoiMOvA9ac2GVdCrSB4LzHk4,29007
47
47
  GameSentenceMiner/util/get_overlay_coords.py,sha256=c37zVKPxFqwIi5XjZKjxRCW_Y8W4e1MX7vSLLwcOhs4,22116
@@ -59,7 +59,7 @@ GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UT
59
59
  GameSentenceMiner/util/win10toast/__init__.py,sha256=6TL2w6rzNmpJEp6_v2cAJP_7ExA3UsKzwdM08pNcVfE,5341
60
60
  GameSentenceMiner/util/win10toast/__main__.py,sha256=5MYnBcFj8y_6Dyc1kiPd0_FsUuh4yl1cv5wsleU6V4w,668
61
61
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
- GameSentenceMiner/web/database_api.py,sha256=pY1j8b1H5VdkDv5aaZjBnpx4tSNfJhfsuukyty6jbi0,84812
62
+ GameSentenceMiner/web/database_api.py,sha256=Ph30uGAJFSxRkBdrZglXKsuwuKP46RxjzGODMO5aaLc,84827
63
63
  GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
64
64
  GameSentenceMiner/web/gsm_websocket.py,sha256=B0VKpxmsRu0WRh5nFWlpDPBQ6-K2ed7TEIa0O6YWeoo,4166
65
65
  GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
@@ -124,9 +124,9 @@ GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic
124
124
  GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json,sha256=8wjnnaYQqmho6t5tMxrIAc03512A2tYhQh5dfsQnfAM,11372
125
125
  GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json,sha256=wRkqZNPzz6DT9OTPHpXwfqW96Qb96stCQNNgOL-ZdKk,17535
126
126
  GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- gamesentenceminer-2.18.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
128
- gamesentenceminer-2.18.3.dist-info/METADATA,sha256=DGaCDSt8zzYwt0sak9LlqbqQ6uGlA8-vHGX6G1XCLTU,7487
129
- gamesentenceminer-2.18.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
130
- gamesentenceminer-2.18.3.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
131
- gamesentenceminer-2.18.3.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
132
- gamesentenceminer-2.18.3.dist-info/RECORD,,
127
+ gamesentenceminer-2.18.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
128
+ gamesentenceminer-2.18.5.dist-info/METADATA,sha256=92I3vL7iPRi0A-TfwXVAETyk3ZzipuX-ZdiyiXkK4_k,7487
129
+ gamesentenceminer-2.18.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
130
+ gamesentenceminer-2.18.5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
131
+ gamesentenceminer-2.18.5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
132
+ gamesentenceminer-2.18.5.dist-info/RECORD,,