GameSentenceMiner 2.18.6__py3-none-any.whl → 2.18.7__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/anki.py CHANGED
@@ -234,7 +234,7 @@ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path
234
234
  sentence = note['fields'].get(config.anki.sentence_field, last_note.get_field(config.anki.sentence_field))
235
235
 
236
236
  use_voice, sentence, translation, new_ss_path = config_app.show_anki_confirmation_dialog(
237
- tango, sentence, assets.screenshot_path, assets.audio_path, translation, ss_time
237
+ tango, sentence, assets.screenshot_path, assets.audio_path if update_audio_flag else None, translation, ss_time
238
238
  )
239
239
  note['fields'][config.anki.sentence_field] = sentence
240
240
  note['fields'][config.ai.anki_field] = translation
@@ -279,7 +279,8 @@ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path
279
279
  sentence_in_anki=game_line.text if game_line else '',
280
280
  multi_line=bool(selected_lines and len(selected_lines) > 1),
281
281
  video_in_anki=assets.video_in_anki or '',
282
- word_path=word_path
282
+ word_path=word_path,
283
+ word=tango
283
284
  )
284
285
 
285
286
  # 9. Update the local application database with final paths
@@ -554,14 +555,17 @@ def update_new_card():
554
555
  else:
555
556
  logger.info("New card(s) detected! Added to Processing Queue!")
556
557
  gsm_state.last_mined_line = game_line
557
- card_queue.append((last_card, datetime.now(), lines))
558
- texthooking_page.reset_checked_lines()
559
- try:
560
- obs.save_replay_buffer()
561
- except Exception as e:
562
- card_queue.pop(0)
563
- logger.error(f"Error saving replay buffer: {e}")
564
- return
558
+ queue_card_for_processing(last_card, lines)
559
+
560
+ def queue_card_for_processing(last_card, lines):
561
+ card_queue.append((last_card, datetime.now(), lines))
562
+ texthooking_page.reset_checked_lines()
563
+ try:
564
+ obs.save_replay_buffer()
565
+ except Exception as e:
566
+ card_queue.pop(0)
567
+ logger.error(f"Error saving replay buffer: {e}")
568
+ return
565
569
 
566
570
  def update_card_from_same_sentence(last_card, lines, game_line):
567
571
  time_elapsed = 0
@@ -570,16 +574,15 @@ def update_card_from_same_sentence(last_card, lines, game_line):
570
574
  time_elapsed += 0.5
571
575
  if time_elapsed > 15:
572
576
  logger.info(f"Timed out waiting for Anki update for card {last_card.noteId}, retrieving new audio")
573
- card_queue.append((last_card, datetime.now(), lines))
574
- texthooking_page.reset_checked_lines()
575
- try:
576
- obs.save_replay_buffer()
577
- except Exception as e:
578
- card_queue.pop(0)
579
- logger.error(f"Error saving replay buffer: {e}")
580
- return
577
+ queue_card_for_processing(last_card, lines)
581
578
  return
582
579
  anki_result = anki_results[game_line.id]
580
+
581
+ if anki_result.word == last_card.get_field(get_config().anki.word_field):
582
+ logger.info(f"Same word detected, attempting to get new audio for card {last_card.noteId}")
583
+ queue_card_for_processing(last_card, lines)
584
+ return
585
+
583
586
  if anki_result.success:
584
587
  note, last_card = get_initial_card_info(last_card, lines)
585
588
  tango = last_card.get_field(get_config().anki.word_field)
@@ -497,11 +497,11 @@
497
497
  },
498
498
  "texthooker_comm_port": {
499
499
  "label": "Texthooker Communication WebSocket Port:",
500
- "tooltip": "Port for GSM Texthooker WebSocket communication. Does nothing right now, hardcoded to 55001"
500
+ "tooltip": "Port for GSM Texthooker WebSocket communication. "
501
501
  },
502
502
  "plaintext_export_port": {
503
- "label": "Plaintext Websocket Export Port:",
504
- "tooltip": "Port for GSM Plaintext WebSocket Export communication. Does nothing right now, hardcoded to 55002"
503
+ "label": "Plaintext/JL Websocket Export Port:",
504
+ "tooltip": "Port that GSM will send plaintext to. Useful for integrating with other tools like JL."
505
505
  },
506
506
  "reset_line_hotkey": {
507
507
  "label": "Reset Line Hotkey:",
@@ -495,12 +495,12 @@
495
495
  "tooltip": "OCR WebSocket通信用のポート。GSMもこのポートで待機します。"
496
496
  },
497
497
  "texthooker_comm_port": {
498
- "label": "Texthooker通信ポート:",
499
- "tooltip": "GSM Texthooker通信用のWebSocketポート(現在未使用)。"
498
+ "label": "Texthooker通信WebSocketポート:",
499
+ "tooltip": "GSM Texthooker WebSocket通信用のポート。"
500
500
  },
501
501
  "plaintext_export_port": {
502
- "label": "平文エクスポートポート:",
503
- "tooltip": "GSM平文エクスポート用のWebSocketポート(現在未使用)。"
502
+ "label": "平文/JL Websocketエクスポートポート:",
503
+ "tooltip": "GSMが平文を送信するポート。JLなどの他のツールとの統合に便利です。"
504
504
  },
505
505
  "reset_line_hotkey": {
506
506
  "label": "行リセットホットキー:",
@@ -497,11 +497,11 @@
497
497
  },
498
498
  "texthooker_comm_port": {
499
499
  "label": "Texthooker 通信 WebSocket 端口:",
500
- "tooltip": "GSM Texthooker WebSocket 通信的端口。目前不起作用,硬编码为 55001。"
500
+ "tooltip": "GSM Texthooker WebSocket 通信的端口。"
501
501
  },
502
502
  "plaintext_export_port": {
503
- "label": "纯文本 WebSocket 导出端口:",
504
- "tooltip": "GSM 纯文本 WebSocket 导出通信的端口。目前不起作用,硬编码为 55002。"
503
+ "label": "纯文本/JL Websocket 导出端口:",
504
+ "tooltip": "GSM 将纯文本发送到的端口。对于与 JL 等其他工具集成很有用。"
505
505
  },
506
506
  "reset_line_hotkey": {
507
507
  "label": "重置行热键:",
@@ -4,6 +4,7 @@ from PIL import Image, ImageTk
4
4
 
5
5
  import ttkbootstrap as ttk
6
6
  from GameSentenceMiner.util.configuration import get_config, logger, gsm_state
7
+ from GameSentenceMiner.util.audio_player import AudioPlayer
7
8
 
8
9
  import platform
9
10
  import subprocess
@@ -21,6 +22,10 @@ class AnkiConfirmationDialog(tk.Toplevel):
21
22
  # Initialize screenshot_path here, will be updated by button if needed
22
23
  self.screenshot_path = screenshot_path
23
24
 
25
+ # Audio player management
26
+ self.audio_player = AudioPlayer(finished_callback=self._audio_finished)
27
+ self.audio_button = None # Store reference to audio button
28
+
24
29
  self.title("Confirm Anki Card Details")
25
30
  self.result = None # This will store the user's choice
26
31
 
@@ -55,6 +60,10 @@ class AnkiConfirmationDialog(tk.Toplevel):
55
60
 
56
61
  self.protocol("WM_DELETE_WINDOW", self._on_cancel)
57
62
  self.attributes('-topmost', True)
63
+
64
+ # Ensure audio cleanup on window close
65
+ self.protocol("WM_DELETE_WINDOW", self._cleanup_and_close)
66
+
58
67
  self.wait_window(self)
59
68
 
60
69
  def _create_widgets(self, expression, sentence, screenshot_path, audio_path, translation):
@@ -76,13 +85,14 @@ class AnkiConfirmationDialog(tk.Toplevel):
76
85
  self.sentence_text = sentence_text
77
86
  row += 1
78
87
 
79
- # Translation
80
- ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
81
- translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
82
- translation_text.insert(tk.END, translation)
83
- translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
84
- self.translation_text = translation_text
85
- row += 1
88
+ if translation:
89
+ # Translation
90
+ ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
91
+ translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
92
+ translation_text.insert(tk.END, translation)
93
+ translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
94
+ self.translation_text = translation_text
95
+ row += 1
86
96
 
87
97
  # Screenshot
88
98
  ttk.Label(main_frame, text=f"{get_config().anki.picture_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
@@ -109,12 +119,20 @@ class AnkiConfirmationDialog(tk.Toplevel):
109
119
  row += 1
110
120
 
111
121
  # Audio Path
112
- ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
113
- ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
114
122
  if audio_path and os.path.isfile(audio_path):
115
- ttk.Button(main_frame, text="Play Audio", command=lambda: self._play_audio(audio_path)).grid(row=row, column=2, sticky="w", padx=5, pady=2)
116
-
117
- row += 1
123
+ ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
124
+ ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
125
+ if audio_path and os.path.isfile(audio_path):
126
+ self.audio_button = ttk.Button(
127
+ main_frame,
128
+ text="▶",
129
+ command=lambda: self._play_audio(audio_path),
130
+ bootstyle="outline-info",
131
+ width=12
132
+ )
133
+ self.audio_button.grid(row=row, column=2, sticky="w", padx=5, pady=2)
134
+
135
+ row += 1
118
136
 
119
137
  # Action Buttons
120
138
  button_frame = ttk.Frame(main_frame)
@@ -161,22 +179,59 @@ class AnkiConfirmationDialog(tk.Toplevel):
161
179
  if not os.path.isfile(audio_path):
162
180
  print(f"Audio file does not exist: {audio_path}")
163
181
  return
182
+
164
183
  try:
165
- if platform.system() == "Windows":
166
- os.startfile(audio_path)
167
- elif platform.system() == "Darwin":
168
- subprocess.run(["open", audio_path])
184
+ # Check if we have a configuration for external audio player
185
+ if get_config().advanced.audio_player_path:
186
+ # Use external audio player
187
+ import platform
188
+ import subprocess
189
+ if platform.system() == "Windows":
190
+ os.startfile(audio_path)
191
+ elif platform.system() == "Darwin":
192
+ subprocess.run(["open", audio_path])
193
+ else:
194
+ subprocess.run(["xdg-open", audio_path])
169
195
  else:
170
- subprocess.run(["xdg-open", audio_path])
196
+ # Use internal audio player
197
+ success = self.audio_player.play_audio_file(audio_path)
198
+ if success:
199
+ self._update_audio_button()
200
+
171
201
  except Exception as e:
172
202
  print(f"Failed to play audio: {e}")
203
+
204
+ def _audio_finished(self):
205
+ """Called when audio playback finishes"""
206
+ self._update_audio_button()
207
+
208
+ def _update_audio_button(self):
209
+ """Update the audio button text and style based on playing state"""
210
+ if self.audio_button:
211
+ if self.audio_player.is_playing:
212
+ self.audio_button.config(text="⏹ Stop", bootstyle="outline-warning")
213
+ else:
214
+ self.audio_button.config(text="▶ Play Audio", bootstyle="outline-info")
215
+
216
+ def _cleanup_audio(self):
217
+ """Clean up audio stream resources"""
218
+ self.audio_player.cleanup()
219
+
220
+ def _cleanup_and_close(self):
221
+ """Clean up resources and close dialog"""
222
+ self._cleanup_audio()
223
+ self._on_cancel()
173
224
 
174
225
  def _on_voice(self):
226
+ # Clean up audio before closing
227
+ self._cleanup_audio()
175
228
  # The screenshot_path is now correctly updated if the user chose a new one
176
229
  self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
177
230
  self.destroy()
178
231
 
179
232
  def _on_no_voice(self):
233
+ # Clean up audio before closing
234
+ self._cleanup_audio()
180
235
  self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
181
236
  self.destroy()
182
237
 
@@ -698,7 +698,7 @@ class ConfigApp:
698
698
  ),
699
699
  overlay=Overlay(
700
700
  websocket_port=int(self.overlay_websocket_port_value.get()),
701
- monitor_to_capture=self.overlay_monitor.current() if self.monitors else 0,
701
+ monitor_to_capture=int(self.overlay_monitor.current() if self.monitors else 0),
702
702
  engine=OverlayEngine(self.overlay_engine_value.get()).value if self.overlay_engine_value.get() else OverlayEngine.LENS.value,
703
703
  scan_delay=float(self.scan_delay_value.get()),
704
704
  periodic=float(self.periodic_value.get()),
@@ -732,6 +732,7 @@ class ConfigApp:
732
732
  self.master_config.set_config_for_profile(current_profile, config)
733
733
 
734
734
  self.master_config.locale = Locale[self.locale_value.get()].value
735
+ self.master_config.overlay = config.overlay
735
736
 
736
737
 
737
738
  config_backup_folder = os.path.join(get_app_directory(), "backup", "config")
@@ -877,6 +878,14 @@ class ConfigApp:
877
878
  ttk.Entry(self.general_tab, textvariable=self.texthooker_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
878
879
  self.current_row += 1
879
880
 
881
+ advanced_i18n = self.i18n.get('tabs', {}).get('advanced', {})
882
+ export_port_i18n = advanced_i18n.get('plaintext_export_port', {})
883
+ HoverInfoLabelWidget(self.general_tab, text=export_port_i18n.get('label', '...'),
884
+ tooltip=export_port_i18n.get('tooltip', '...'),
885
+ row=self.current_row, column=0)
886
+ ttk.Entry(self.general_tab, textvariable=self.plaintext_websocket_export_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
887
+ self.current_row += 1
888
+
880
889
  # locale_i18n = general_i18n.get('locale', {})
881
890
  # HoverInfoLabelWidget(self.general_tab, text=locale_i18n.get('label', 'Locale:'),
882
891
  # tooltip=locale_i18n.get('tooltip', '...'),
@@ -2047,13 +2056,6 @@ class ConfigApp:
2047
2056
  ttk.Entry(advanced_frame, textvariable=self.texthooker_communication_websocket_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2048
2057
  self.current_row += 1
2049
2058
 
2050
- export_port_i18n = advanced_i18n.get('plaintext_export_port', {})
2051
- HoverInfoLabelWidget(advanced_frame, text=export_port_i18n.get('label', '...'),
2052
- tooltip=export_port_i18n.get('tooltip', '...'),
2053
- row=self.current_row, column=0)
2054
- ttk.Entry(advanced_frame, textvariable=self.plaintext_websocket_export_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2055
- self.current_row += 1
2056
-
2057
2059
  reset_hotkey_i18n = advanced_i18n.get('reset_line_hotkey', {})
2058
2060
  HoverInfoLabelWidget(advanced_frame, text=reset_hotkey_i18n.get('label', '...'),
2059
2061
  tooltip=reset_hotkey_i18n.get('tooltip', '...'), row=self.current_row, column=0)
@@ -2342,13 +2344,14 @@ class ConfigApp:
2342
2344
 
2343
2345
  overlay_monitor_i18n = overlay_i18n.get('overlay_monitor', {})
2344
2346
  HoverInfoLabelWidget(overlay_frame, text=overlay_monitor_i18n.get('label', '...'),
2345
- tooltip=overlay_monitor_i18n.get('tooltip', '...'),
2346
- row=self.current_row, column=0)
2347
- self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors, state="readonly")
2347
+ tooltip=overlay_monitor_i18n.get('tooltip', '...'),
2348
+ row=self.current_row, column=0)
2349
+ self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors)
2348
2350
  self.overlay_monitor.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2349
2351
  # disable selection for now, default to value 1
2350
- self.overlay_monitor.current(0)
2351
- self.overlay_monitor.config(state="disabled")
2352
+ if self.settings.overlay.monitor_to_capture >= len(self.monitors):
2353
+ self.settings.overlay.monitor_to_capture = 0
2354
+ self.overlay_monitor.current(self.settings.overlay.monitor_to_capture)
2352
2355
  self.current_row += 1
2353
2356
 
2354
2357
  # Overlay Engine Selection
@@ -0,0 +1,220 @@
1
+ """
2
+ Audio playback utility module for GameSentenceMiner.
3
+ Provides safe, non-blocking audio playback functionality.
4
+ """
5
+
6
+ import threading
7
+ from typing import Optional, Callable
8
+ import sounddevice as sd
9
+ import soundfile as sf
10
+ import numpy as np
11
+
12
+
13
+ class AudioPlayer:
14
+ """
15
+ A safe, non-blocking audio player class that handles audio stream management.
16
+ """
17
+
18
+ def __init__(self, finished_callback: Optional[Callable] = None):
19
+ """
20
+ Initialize the audio player.
21
+
22
+ Args:
23
+ finished_callback: Optional callback function to call when audio finishes playing
24
+ """
25
+ self.current_audio_stream: Optional[sd.OutputStream] = None
26
+ self.current_audio_data: Optional[np.ndarray] = None
27
+ self.current_audio_samplerate: Optional[int] = None
28
+ self.is_playing: bool = False
29
+ self._audio_position: int = 0
30
+ self.finished_callback = finished_callback
31
+ self._lock = threading.Lock()
32
+
33
+ def play_audio_file(self, audio_path: str) -> bool:
34
+ """
35
+ Play an audio file. If already playing, stop the current playback.
36
+
37
+ Args:
38
+ audio_path: Path to the audio file to play
39
+
40
+ Returns:
41
+ True if playback started successfully, False otherwise
42
+ """
43
+ try:
44
+ # If audio is currently playing, stop it
45
+ if self.is_playing:
46
+ self.stop_audio()
47
+ return True
48
+
49
+ # Load audio data
50
+ data, samplerate = sf.read(audio_path)
51
+ return self.play_audio_data(data, samplerate)
52
+
53
+ except Exception as e:
54
+ print(f"Failed to play audio file {audio_path}: {e}")
55
+ self._cleanup_stream()
56
+ return False
57
+
58
+ def play_audio_data(self, data: np.ndarray, samplerate: int) -> bool:
59
+ """
60
+ Play audio from numpy array data.
61
+
62
+ Args:
63
+ data: Audio data as numpy array
64
+ samplerate: Sample rate of the audio data
65
+
66
+ Returns:
67
+ True if playback started successfully, False otherwise
68
+ """
69
+ try:
70
+ with self._lock:
71
+ # Ensure data is float32 for sounddevice playback
72
+ data = data.astype('float32')
73
+
74
+ # Store audio data
75
+ self.current_audio_data = data
76
+ self.current_audio_samplerate = samplerate
77
+ self._audio_position = 0
78
+
79
+ # Create audio callback
80
+ def audio_callback(outdata, frames, time, status):
81
+ if status:
82
+ print(f"Audio callback status: {status}")
83
+
84
+ # Calculate how much data we need for this callback
85
+ start_frame = self._audio_position
86
+ end_frame = start_frame + frames
87
+
88
+ if end_frame <= len(data):
89
+ # We have enough data
90
+ if data.ndim == 1:
91
+ outdata[:, 0] = data[start_frame:end_frame]
92
+ else:
93
+ outdata[:] = data[start_frame:end_frame]
94
+ self._audio_position = end_frame
95
+ else:
96
+ # We've reached the end
97
+ remaining_frames = len(data) - start_frame
98
+ if remaining_frames > 0:
99
+ if data.ndim == 1:
100
+ outdata[:remaining_frames, 0] = data[start_frame:]
101
+ outdata[remaining_frames:, 0] = 0
102
+ else:
103
+ outdata[:remaining_frames] = data[start_frame:]
104
+ outdata[remaining_frames:] = 0
105
+ else:
106
+ outdata.fill(0)
107
+
108
+ # Schedule cleanup
109
+ self._schedule_finish()
110
+
111
+ # Create and start audio stream
112
+ stream = sd.OutputStream(
113
+ samplerate=samplerate,
114
+ channels=data.shape[1] if data.ndim > 1 else 1,
115
+ callback=audio_callback
116
+ )
117
+
118
+ self.current_audio_stream = stream
119
+ self.is_playing = True
120
+ stream.start()
121
+
122
+ return True
123
+
124
+ except Exception as e:
125
+ print(f"Failed to play audio data: {e}")
126
+ self._cleanup_stream()
127
+ return False
128
+
129
+ def stop_audio(self):
130
+ """Stop the currently playing audio."""
131
+ with self._lock:
132
+ if self.current_audio_stream and self.is_playing:
133
+ try:
134
+ self.current_audio_stream.stop()
135
+ except Exception:
136
+ pass
137
+ self._cleanup_stream()
138
+
139
+ def _schedule_finish(self):
140
+ """Schedule the finish callback to be called."""
141
+ # Use threading to avoid blocking the audio callback
142
+ def finish_task():
143
+ self._audio_finished()
144
+
145
+ threading.Thread(target=finish_task, daemon=True).start()
146
+
147
+ def _audio_finished(self):
148
+ """Called when audio playback finishes."""
149
+ with self._lock:
150
+ self._cleanup_stream()
151
+ if self.finished_callback:
152
+ try:
153
+ self.finished_callback()
154
+ except Exception as e:
155
+ print(f"Error in audio finished callback: {e}")
156
+
157
+ def _cleanup_stream(self):
158
+ """Clean up the current audio stream."""
159
+ if self.current_audio_stream:
160
+ try:
161
+ self.current_audio_stream.close()
162
+ except Exception:
163
+ pass
164
+
165
+ self.current_audio_stream = None
166
+ self.is_playing = False
167
+ self.current_audio_data = None
168
+ self.current_audio_samplerate = None
169
+ self._audio_position = 0
170
+
171
+ def cleanup(self):
172
+ """Clean up all resources."""
173
+ self.stop_audio()
174
+ self.finished_callback = None
175
+
176
+
177
+ def create_safe_audio_callback(data: np.ndarray, position_tracker: dict, finish_callback: Callable):
178
+ """
179
+ Create a safe audio callback function for use with sounddevice.
180
+
181
+ Args:
182
+ data: Audio data array
183
+ position_tracker: Dictionary to track playback position (mutable)
184
+ finish_callback: Function to call when playback finishes
185
+
186
+ Returns:
187
+ Audio callback function
188
+ """
189
+ def audio_callback(outdata, frames, time, status):
190
+ if status:
191
+ print(f"Audio callback status: {status}")
192
+
193
+ # Calculate how much data we need for this callback
194
+ start_frame = position_tracker.get('position', 0)
195
+ end_frame = start_frame + frames
196
+
197
+ if end_frame <= len(data):
198
+ # We have enough data
199
+ if data.ndim == 1:
200
+ outdata[:, 0] = data[start_frame:end_frame]
201
+ else:
202
+ outdata[:] = data[start_frame:end_frame]
203
+ position_tracker['position'] = end_frame
204
+ else:
205
+ # We've reached the end
206
+ remaining_frames = len(data) - start_frame
207
+ if remaining_frames > 0:
208
+ if data.ndim == 1:
209
+ outdata[:remaining_frames, 0] = data[start_frame:]
210
+ outdata[remaining_frames:, 0] = 0
211
+ else:
212
+ outdata[:remaining_frames] = data[start_frame:]
213
+ outdata[remaining_frames:] = 0
214
+ else:
215
+ outdata.fill(0)
216
+
217
+ # Schedule cleanup
218
+ threading.Thread(target=finish_callback, daemon=True).start()
219
+
220
+ return audio_callback
@@ -666,7 +666,17 @@ class Overlay:
666
666
  def __post_init__(self):
667
667
  if self.monitor_to_capture == -1:
668
668
  self.monitor_to_capture = 0 # Default to the first monitor if not set
669
-
669
+
670
+ try:
671
+ import mss as mss
672
+ monitors = [f"Monitor {i}: width: {monitor['width']}, height: {monitor['height']}" for i, monitor in enumerate(mss.mss().monitors[1:], start=1)]
673
+ if len(monitors) == 0:
674
+ monitors = [1]
675
+ self.monitors = monitors
676
+ except ImportError:
677
+ self.monitors = []
678
+ if self.monitor_to_capture >= len(self.monitors):
679
+ self.monitor_to_capture = 0 # Reset to first monitor if out of range
670
680
 
671
681
  @dataclass_json
672
682
  @dataclass
@@ -823,6 +833,7 @@ class Config:
823
833
  switch_to_default_if_not_found: bool = True
824
834
  locale: str = Locale.English.value
825
835
  stats: StatsConfig = field(default_factory=StatsConfig)
836
+ overlay: Overlay = field(default_factory=Overlay)
826
837
  version: str = ""
827
838
 
828
839
  @classmethod
@@ -865,7 +876,7 @@ class Config:
865
876
  if self.version:
866
877
  if self.version != get_current_version():
867
878
  from packaging import version
868
- logger.info(f"Config version mismatch detected: {self.version} != {get_current_version()}")
879
+ logger.info(f"New Config Found: {self.version} != {get_current_version()}")
869
880
  # Handle version mismatch
870
881
  changed = False
871
882
  if version.parse(self.version) < version.parse("2.18.0"):
@@ -879,6 +890,7 @@ class Config:
879
890
 
880
891
  if changed:
881
892
  self.save()
893
+ self.overlay = self.get_config().overlay
882
894
 
883
895
  self.version = get_current_version()
884
896
 
@@ -1160,6 +1172,11 @@ def get_config():
1160
1172
  # print(config_instance.get_config())
1161
1173
  return config_instance.get_config()
1162
1174
 
1175
+ def get_overlay_config():
1176
+ global config_instance
1177
+ if config_instance is None:
1178
+ config_instance = load_config()
1179
+ return config_instance.overlay
1163
1180
 
1164
1181
  def reload_config():
1165
1182
  global config_instance
@@ -1284,6 +1301,7 @@ class GsmAppState:
1284
1301
  self.recording_started_time = None
1285
1302
  self.current_srt = None
1286
1303
  self.srt_index = 1
1304
+ self.current_audio_stream = None
1287
1305
 
1288
1306
 
1289
1307
  @dataclass_json
@@ -1297,10 +1315,11 @@ class AnkiUpdateResult:
1297
1315
  multi_line: bool = False
1298
1316
  video_in_anki: str = ''
1299
1317
  word_path: str = ''
1318
+ word: str = ''
1300
1319
 
1301
1320
  @staticmethod
1302
1321
  def failure():
1303
- return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False, video_in_anki='', word_path='')
1322
+ return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False, video_in_anki='', word_path='', word='')
1304
1323
 
1305
1324
 
1306
1325
  @dataclass_json